1# friends - irssi 0.8.4.CVS
2#
3#    $Id: friends.pl,v 1.34 2004/03/08 21:47:12 peder Exp $
4#
5# Copyright (C) 2001, 2002, 2003 by Peder Stray <peder@ninja.no>
6#
7
8use strict;
9use Irssi 20020427.2353;
10use Irssi::Irc;
11use Irssi::TextUI;
12
13use Data::Dumper;
14$Data::Dumper::Indent = 1;
15
16# ======[ Script Header ]===============================================
17
18use vars qw{$VERSION %IRSSI};
19($VERSION) = '$Revision: 1.34 $' =~ / (\d+\.\d+) /;
20%IRSSI = (
21          name        => 'friends',
22          authors     => 'Peder Stray',
23          contact     => 'peder@ninja.no',
24          url         => 'http://ninja.no/irssi/friends.pl',
25          license     => 'GPL',
26          description => 'Basicly an autoop script with a nice interface and nick coloring ;)',
27         );
28
29# ======[ Variables ]===================================================
30
31my(%friends, @friends);
32
33my(%flagshort) = (
34		  op => 'o',
35		  voice => 'v',
36		  color => 'c',
37		 );
38my(%flaglong) = map { $flagshort{$_} => $_ } keys %flagshort;
39
40# ======[ Helper functions ]============================================
41
42# --------[ crap ]------------------------------------------------------
43
44sub crap {
45    my $template = shift;
46    my $msg = sprintf $template, @_;
47    Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'friends_crap', $msg);
48}
49
50# --------[ load_friends ]----------------------------------------------
51
52sub load_friends {
53    my($file) = Irssi::get_irssi_dir."/friends";
54    my($count) = 0;
55    my($mask,$net,$channel,$flags,$flag);
56    local(*FILE);
57
58    %friends = ();
59    open FILE, "<", $file;
60    while (<FILE>) {
61	($mask,$net,$channel,$flags) = split;
62	for (split //, $flags) {
63	    if ($flag = $flaglong{$_}) {
64		$friends{$mask}{lc $net}{lc $channel}{$flag} = 1;
65	    }
66	}
67    }
68    close FILE;
69    $count = keys %friends;
70
71    crap("Loaded $count friends from $file");
72}
73
74# --------[ save_friends ]----------------------------------------------
75
76sub save_friends {
77    my($auto) = @_;
78    my($file) = Irssi::get_irssi_dir."/friends";
79    my($count) = 0;
80    local(*FILE);
81
82    return if $auto && !Irssi::settings_get_bool('friends_autosave');
83
84    open FILE, ">", $file;
85    for my $mask (keys %friends) {
86	$count++;
87	for my $net (keys %{$friends{$mask}}) {
88	    for my $channel (keys %{$friends{$mask}{$net}}) {
89		print FILE "$mask\t$net\t$channel\t".
90		  join("", sort map {$flagshort{$_}} keys %{$friends{$mask}{$net}{$channel}}).
91		    "\n";
92	    }
93	}
94    }
95    close FILE;
96
97    crap("Saved $count friends to $file")
98      unless $auto;
99}
100
101# --------[ is_friends_window ]-----------------------------------------
102
103sub is_friends_window {
104    my($win) = @_;
105    return $win->{name} eq '<Friends>';
106}
107
108# --------[ get_friends_window ]----------------------------------------
109
110sub get_friends_window {
111    my($win) = Irssi::window_find_name('<Friends>');
112    if ($win) {
113	$win->set_active;
114    } else {
115	Irssi::command("window new hide");
116	$win = Irssi::active_win;
117	$win->set_name('<Friends>');
118	$win->set_history('<Friends>');
119    }
120    return $win;
121}
122
123# --------[ get_friend ]------------------------------------------------
124
125sub get_friend {
126    my($channel,$nick) = @_;
127    my($server) = $channel->{server};
128    my($chan) = lc $channel->{name};
129    my($net) = lc $server->{chatnet};
130    my($flags,@friend);
131
132    for my $mask (keys %friends) {
133	next unless $server->mask_match_address($mask,
134						$nick->{nick},
135						$nick->{host});
136	for my $n ('*', $net) {
137	    for my $c ('*', $chan) {
138		if (exists $friends{$mask}{$n}{$c}) {
139		    for my $flag (keys %{$friends{$mask}{$n}{$c}}) {
140			$flags->{$flag} = 1;
141		    }
142		}
143	    }
144	}
145	return $flags if $flags;
146    }
147    return undef;
148}
149
150# --------[ check_friends ]---------------------------------------------
151
152sub check_friends {
153    my($channel, @nicks) = @_;
154    my(%op,%voice);
155    my($nick,$friend,$list);
156    my(@friends);
157
158    return unless $channel->{chanop} || $channel->{ownnick}{op};
159
160    for $nick (@nicks) {
161	$friend = get_friend($channel, $nick);
162	next unless $friend;
163	next if $nick->{nick} eq $channel->{server}{nick};
164	if ($friend->{op} && !$nick->{op}) {
165	    $op{$nick->{nick}} = 1;
166	}
167	if ($friend->{voice} && !$nick->{voice}) {
168	    $voice{$nick->{nick}} = 1;
169	}
170	push @friends, ($nick->{op}?'@':'').
171	  ($nick->{voice}?'+':'').$nick->{nick};
172    }
173
174    if (@friends && Irssi::settings_get_bool("friends_show_check")) {
175	my($max) = Irssi::settings_get_int("friends_max_nicks");
176	@friends = sort @friends;
177	$channel->printformat(MSGLEVEL_CLIENTCRAP,
178			      @friends>$max
179			      ? 'friends_check_more' : 'friends_check',
180			      join(" ", splice @friends, 0, $max),
181			      scalar @friends);
182    }
183
184    if ($list = join " ", sort keys %op) {
185        $channel->command("op $list");
186    }
187    if ($list = join " ", sort keys %voice) {
188        $channel->command("voice $list");
189    }
190}
191
192# --------[ update_friends_hash ]---------------------------------------
193
194sub update_friends_hash {
195    %friends = ();
196    for (@friends) {
197	my($num,$mask,$chan,$net,$flags) = @$_;
198	for (split //, $flags) {
199	    $friends{$mask}{$net}{$chan}{$flaglong{$_}} = 1;
200	}
201    }
202}
203
204# --------[ update_friends_window ]-------------------------------------
205
206sub update_friends_window {
207    my($win) = Irssi::window_find_name('<Friends>');
208    my($view);
209    my($num) = 0;
210    my($mask,$net,$channel,$flags);
211
212    my(%net);
213
214    if ($win) {
215	@friends = ();
216	for $mask (sort keys %friends) {
217	    for $net (sort keys %{$friends{$mask}}) {
218		for $channel (sort keys %{$friends{$mask}{$net}}) {
219		    $flags = join "", sort map {$flagshort{$_}}
220		      keys %{$friends{$mask}{$net}{$channel}};
221		    push @friends, [ ++$num, $mask, $channel, $net, $flags ];
222		}
223	    }
224	}
225
226	$view = $win->view;
227	$view->remove_all_lines();
228	$view->clear();
229	$win->printformat(MSGLEVEL_NEVER, 'friends_header',
230			  '##', 'Mask', 'Channel', 'ChatNet', 'Flags');
231	for (@friends) {
232	    ($num,$mask,$channel,$net,$flags) = @$_;
233	    if (!$net{$net}) {
234		my($n) = Irssi::chatnet_find($net);
235		$net{$net} = $n?$n->{name}:$net;
236	    }
237	    $win->printformat(MSGLEVEL_NEVER, 'friends_line',
238			      $num, $mask, $channel, $net{$net}, $flags);
239	}
240	$win->printformat(MSGLEVEL_NEVER, 'friends_footer', scalar @friends);
241    }
242}
243
244# ======[ Signal Hooks ]================================================
245
246# --------[ sig_send_command ]------------------------------------------
247
248sub sig_send_command {
249    my($win) = Irssi::active_win;
250    if (is_friends_window($win)) {
251	my($cmd,@param) = split " ", $_[0];
252	my($changed) = 0;
253
254	Irssi::signal_stop;
255
256	for (lc $cmd) {
257	    s,^/,,;
258	    if (/^m(ask)?$/) {
259		$changed = subcmd_friends_mask($win,@param);
260
261	    } elsif (/^c(han(nel)?)?$/) {
262		$changed = subcmd_friends_channel($win,@param);
263
264	    } elsif (/^(?:n(et)?|chat(net)?)$/) {
265		$changed = subcmd_friends_net($win,@param);
266
267	    } elsif (/^del(ete)?$/) {
268		$changed = subcmd_friends_delete($win,@param);
269
270	    } elsif (/^f(lags?)?$/) {
271		$changed = subcmd_friends_flags($win,@param);
272
273	    } elsif (/^s(ave)?/) {
274		save_friends();
275
276	    } elsif (/^(?:e(xit)?|q(uit)?)$/) {
277		$win->destroy;
278
279	    } elsif (/^(?:h(elp)?|\?)$/) {
280		subcmd_friends_help($win);
281
282	    } else {
283		$win->print("CMD: $cmd @{[map{\"[$_]\"}@param]}");
284
285	    }
286	}
287
288	if ($changed) {
289	    update_friends_hash();
290	    update_friends_window();
291	    save_friends(1);
292	}
293    }
294}
295
296# --------[ sig_massjoin ]----------------------------------------------
297
298sub sig_massjoin {
299    my($channel, $nicks) = @_;
300    check_friends($channel, @$nicks);
301}
302
303# --------[ sig_nick_mode_changed ]-------------------------------------
304
305sub sig_nick_mode_changed {
306    my($channel, $nick) = @_;
307    if ($channel->{synced} && $channel->{server}{nick} eq $nick->{nick}) {
308	check_friends($channel, $channel->nicks);
309    }
310}
311
312# --------[ sig_channel_sync ]------------------------------------------
313
314sub sig_channel_sync {
315    my($channel) = @_;
316    check_friends($channel, $channel->nicks);
317}
318
319# --------[ sig_setup_reread ]------------------------------------------
320
321sub sig_setup_reread {
322    load_friends;
323}
324
325# --------[ sig_setup_save ]--------------------------------------------
326
327sub sig_setup_save {
328    my($mainconf,$auto) = @_;
329    save_friends($auto);
330}
331
332# --------[ sig_window_changed ]----------------------------------------
333
334sub sig_window_changed {
335    my($new,$old) = @_;
336    if (is_friends_window($new)) {
337	update_friends_window();
338    }
339}
340
341# --------[ sig_message_public ]----------------------------------------
342
343sub sig_message_public {
344    my($server, $msg, $nick, $addr, $target) = @_;
345    my($window,$theme,$friend,$oform,$nform);
346    my($channel) = $server->channel_find($target);
347
348    return unless $channel;
349
350    my($color) = Irssi::settings_get_str("friends_nick_color");
351
352    $friend = get_friend($channel, $channel->nick_find($nick));
353
354    if ($friend && $color =~ /^[rgbcmykpwRGBCMYKPWFU0-9_]$/) {
355	$window = $server->window_find_item($target);
356	$theme = $window->{theme} || Irssi::current_theme;
357
358	$oform = $nform = $theme->get_format('fe-common/core', 'pubmsg');
359	$nform =~ s/(\$(\[-?\d+\])?0)/%$color$1%n/g;
360
361	$window->command("^format pubmsg $nform");
362	Irssi::signal_continue(@_);
363	$window->command("^format pubmsg $oform");
364    }
365}
366
367# --------[ sig_message_irc_action ]------------------------------------
368
369sub sig_message_irc_action {
370    my($server, $msg, $nick, $addr, $target) = @_;
371    my($window,$theme,$friend,$oform,$nform);
372    my($channel) = $server->channel_find($target);
373
374    return unless $channel;
375
376    my($color) = Irssi::settings_get_str("friends_nick_color");
377
378    $friend = get_friend($channel, $channel->nick_find($nick));
379
380    if ($friend && $color =~ /^[rgbcmykpwRGBCMYKPWFU0-9_]$/) {
381	$window = $server->window_find_item($target);
382	$theme = $window->{theme} || Irssi::current_theme;
383
384	$oform = $nform = $theme->get_format('fe-common/irc',
385					     'action_public');
386	$nform =~ s/(\$(\[-?\d+\])?0)/%$color$1%n/g;
387
388	$window->command("^format action_public $nform");
389	Irssi::signal_continue(@_);
390	$window->command("^format action_public $oform");
391    }
392}
393
394# ======[ Commands ]====================================================
395
396# --------[ FRIENDS ]---------------------------------------------------
397
398# Usage: /FRIENDS
399sub cmd_friends {
400    my($win) = get_friends_window;
401    update_friends_window();
402}
403
404# --------[ subcmd_friends_channel ]------------------------------------
405
406sub subcmd_friends_channel {
407    my($win,$num,$chan) = @_;
408
409    unless ($chan && defined $num) {
410	$win->print("Syntax: CHANNEL <num> <channel>", MSGLEVEL_NEVER);
411	return;
412    }
413
414    unless (0 < $num && $num <= @friends) {
415	$win->print("Error: Element $num not in list", MSGLEVEL_NEVER);
416	return;
417    }
418
419    $friends[$num-1][2] = $chan;
420
421    return 1;
422}
423
424# --------[ subcmd_friends_delete ]-------------------------------------
425
426sub subcmd_friends_delete {
427    my($win,$num) = @_;
428
429    unless (defined $num) {
430	$win->print("Syntax: DELETE <num>", MSGLEVEL_NEVER);
431	return;
432    }
433
434    unless (0 < $num && $num <= @friends) {
435	$win->print("Error: Element $num not in list", MSGLEVEL_NEVER);
436	return;
437    }
438
439    splice @friends, $num-1, 1;
440
441    return 1;
442}
443
444# --------[ subcmd_friends_flags ]--------------------------------------
445
446sub subcmd_friends_flags {
447    my($win,$num,$flags) = @_;
448    my(%f);
449
450    unless ($flags && defined $num) {
451	$win->print("Syntax: FLAGS <num> <flags>", MSGLEVEL_NEVER);
452	return;
453    }
454
455    unless (0 < $num && $num <= @friends) {
456	$win->print("Error: Element $num not in list", MSGLEVEL_NEVER);
457	return;
458    }
459
460    $friends[$num-1][4] = join "", sort grep {!$f{$_}++}
461      split //, $flags;
462
463    return 1;
464}
465
466# --------[ subcmd_friends_help ]---------------------------------------
467
468sub subcmd_friends_help {
469    my($win) = @_;
470
471    $win->print(q{CHANNEL <num> <channel>    - set channel
472
473    <channel> is either a channel name or * for all
474}, MSGLEVEL_NEVER);
475
476    $win->print(q{DELETE  <num>              - delete entry
477}, MSGLEVEL_NEVER);
478
479    $win->print(q{FLAGS   <num> <flags>      - set flags
480
481    <flags> is a list of c (color), o (give op), v (give voice)
482}, MSGLEVEL_NEVER);
483
484    $win->print(q{MASK    <num> <mask>       - set mask
485
486    <mask> is in the usual nick!user@host format
487}, MSGLEVEL_NEVER);
488
489    $win->print(q{NET     <num> <net>        - set net
490
491   <net> is one of your defined ircnets or * for all
492}, MSGLEVEL_NEVER);
493
494}
495
496# --------[ subcmd_friends_mask ]---------------------------------------
497
498sub subcmd_friends_mask {
499    my($win, $num, $mask) = @_;
500
501    unless ($mask && defined $num) {
502	$win->print("Syntax: MASK <num> <mask>", MSGLEVEL_NEVER);
503	return;
504    }
505
506    unless (0 < $num && $num <= @friends) {
507	$win->print("Error: Element $num not in list", MSGLEVEL_NEVER);
508	return;
509    }
510
511    unless ($mask =~ /^.+!.+@.+$/) {
512	$win->print("Error: Mask $mask is not valid", MSGLEVEL_NEVER);
513    }
514
515    $friends[$num-1][1] = $mask;
516
517    return 1;
518}
519
520# --------[ subcmd_friends_net ]----------------------------------------
521
522sub subcmd_friends_net {
523    my($win,$num,$net) = @_;
524    my($n);
525
526    unless ($net && defined $num) {
527	$win->print("Syntax: NET <num> <net>", MSGLEVEL_NEVER);
528	return;
529    }
530
531    unless (0 < $num && $num <= @friends) {
532	$win->print("Error: Element $num not in list", MSGLEVEL_NEVER);
533	return;
534    }
535
536    if ($net eq '*') {
537	# all is well
538    } elsif ($n = Irssi::chatnet_find($net)) {
539	$net = $n->{name};
540    } else {
541	$win->print("Error: No defined chatnet named $net",
542		    MSGLEVEL_NEVER);
543	return;
544    }
545
546    $friends[$num-1][3] = $net;
547
548    return 1;
549}
550
551# --------[ ADDFRIEND ]-------------------------------------------------
552
553# Usage: /ADDFRIEND <nick>|<mask> [<channel>|* [<net>|*]]
554#                                 [-mask host|normal|domain|full]
555#			          [-flags <flags>]
556sub cmd_addfriend {
557    my($param,$serv,$chan) = @_;
558    my(@param,@flags);
559    my($type) = Irssi::Irc::MASK_USER | Irssi::Irc::MASK_DOMAIN;
560    my($mask,$flags,$channel,$net);
561    my(@split) = split " ", $param;
562
563    while (@split) {
564	$_ = shift @split;
565	if (/^-m(ask)?$/) {
566	    $_ = shift @split;
567	    if (/^h(ost)?$/) {
568		$type = Irssi::Irc::MASK_HOST;
569	    } elsif (/^n(ormal)?$/) {
570		$type = Irssi::Irc::MASK_USER
571	              | Irssi::Irc::MASK_DOMAIN;
572	    } elsif (/^d(omain)?$/) {
573		$type = Irssi::Irc::MASK_DOMAIN;
574	    } elsif (/^f(ull)?$/) {
575		$type = Irssi::Irc::MASK_NICK
576	              | Irssi::Irc::MASK_USER
577		      | Irssi::Irc::MASK_HOST;
578	    } else {
579		# fjekk
580	    }
581	} elsif (/^-flags?$/) {
582	    $flags = shift @split;
583	} else {
584	    push @param, $_;
585	}
586    }
587    ($mask,$channel,$net) = @param;
588
589    unless ($mask) {
590	crap("/ADDFRIEND [-mask full|normal|host|domain] [-flags <[o][v][c]>] <nick|mask> [<channel> [<chatnet>]]]");
591	return;
592    }
593
594    $flags ||= "o";
595
596    unless ($channel) {
597	if ($chan) {
598	    $channel = $chan->{name};
599	} else {
600	    crap("/ADDFRIEND needs a channel.");
601	    return;
602	}
603    }
604
605    unless ($net) {
606	if ($serv) {
607	    $net = $serv->{chatnet};
608	} else {
609	    crap("/ADDFRIEND needs a chatnet.");
610	    return;
611	}
612    }
613
614    # is this a nick we need to expand?
615    unless ($mask =~ /.+!.+@.+/) {
616	my($nick);
617	if ($net ne '*') {
618	    unless ($serv = Irssi::server_find_chatnet($net)) {
619		crap("Error locating server for $net.");
620		return;
621	    }
622	} else {
623	    unless ($serv) {
624		crap("Need a server for nick expansion");
625		return
626	    }
627	}
628	if ($channel ne '*') {
629	    unless ($chan = $serv->channel_find($channel)) {
630		crap("Error locating channel $channel.");
631		return;
632	    }
633	} else {
634	    unless ($chan) {
635		crap("Need a channel for nick expansion");
636		return;
637	    }
638	}
639	unless ($nick = $chan->nick_find($mask)) {
640	    crap("Error locating nick $mask.");
641	    return;
642	}
643	$mask = Irssi::Irc::get_mask($nick->{nick}, $nick->{host}, $type);
644    }
645
646    for my $flag (split //, $flags) {
647	unless ($flag = $flaglong{$flag}) {
648	    crap("Unknown flag [$flag]");
649	    next;
650	}
651	push @flags, $flag;
652	$friends{$mask}{lc $net}{lc $channel}{$flag} = 1;
653    }
654
655    if (@flags) {
656	crap("Added %s for %s in %s on %s.",
657	     join(",", @flags), $mask, $channel, $net);
658    }
659
660    save_friends(1);
661}
662
663# ======[ Setup ]=======================================================
664
665# --------[ Register settings ]-----------------------------------------
666
667Irssi::settings_add_bool('friends', 'friends_autosave', 1);
668Irssi::settings_add_int('friends', 'friends_max_nicks', 10);
669Irssi::settings_add_bool('friends', 'friends_show_check', 1);
670
671Irssi::settings_add_str('friends', 'friends_nick_color', '');
672
673# --------[ Register formats ]------------------------------------------
674
675Irssi::theme_register(
676[
677 'friends_crap',
678 '{line_start}{hilight Friends:} $0',
679
680 'friends_check',
681 '{line_start}{hilight Friends} checked: $0',
682
683 'friends_check_more',
684 '{line_start}{hilight Friends} checked: $0 (+$1 more)',
685
686 'friends_header',
687 '<%W$[2]0%n> <%W$[33]1%n> <%W$[13]2%n> <%W$[13]3%n> <%W$[5]4%n>',
688
689 'friends_line',
690 '[%R$[-2]0%n] $[35]1 $[15]2 $[15]3 $[7]4',
691
692 'friends_footer',
693 "\n".'%4 List contains $0 friends %>%n',
694
695]);
696
697# --------[ Register signals ]------------------------------------------
698
699Irssi::signal_add_first("send command", "sig_send_command");
700
701Irssi::signal_add_last("massjoin", "sig_massjoin");
702Irssi::signal_add_last("nick mode changed", "sig_nick_mode_changed");
703Irssi::signal_add_last("channel sync", "sig_channel_sync");
704
705Irssi::signal_add('setup saved', 'sig_setup_save');
706Irssi::signal_add('setup reread', 'sig_setup_reread');
707
708Irssi::signal_add('window changed', 'sig_window_changed');
709
710Irssi::signal_add_first('message public', 'sig_message_public');
711Irssi::signal_add_first('message irc action', 'sig_message_irc_action');
712
713# --------[ Register commands ]-----------------------------------------
714
715Irssi::command_bind('friends', 'cmd_friends');
716Irssi::command_bind('addfriend', 'cmd_addfriend');
717
718# --------[ Register timers ]-------------------------------------------
719
720# --------[ Load config ]-----------------------------------------------
721
722load_friends;
723
724# ======[ END ]=========================================================
725
726# Local Variables:
727# header-initial-hide: t
728# mode: header-minor
729# end:
730