# # Copyright (C) 2001-2021 by Peder Stray # use strict; use Irssi 20020427.2353; use Irssi::Irc; use Irssi::TextUI; use vars qw{$VERSION %IRSSI}; ($VERSION) = '$Revision: 1.34.1 $' =~ / (\d+(\.\d+)+) /; %IRSSI = ( name => 'friends', authors => 'Peder Stray', contact => 'peder.stray@gmail.com', url => 'https://github.com/pstray/irssi-friends', license => 'GPL', description => 'Basically an autoop script with a nice interface and nick coloring ;)', ); my(%friends, @friends); my(%flagshort) = ( op => 'o', voice => 'v', color => 'c', ); my(%flaglong) = map { $flagshort{$_} => $_ } keys %flagshort; sub crap { my $template = shift; my $msg = sprintf $template, @_; Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'friends_crap', $msg); } sub load_friends { my($file) = Irssi::get_irssi_dir."/friends"; my($count) = 0; my($mask,$net,$channel,$flags,$flag); local(*FILE); %friends = (); open FILE, "<", $file; while () { ($mask,$net,$channel,$flags) = split; for (split //, $flags) { if ($flag = $flaglong{$_}) { $friends{$mask}{lc $net}{lc $channel}{$flag} = 1; } } } close FILE; $count = keys %friends; crap("Loaded $count friends from $file"); } sub save_friends { my($auto) = @_; my($file) = Irssi::get_irssi_dir."/friends"; my($count) = 0; local(*FILE); return if $auto && !Irssi::settings_get_bool('friends_autosave'); open FILE, ">", $file; for my $mask (keys %friends) { $count++; for my $net (keys %{$friends{$mask}}) { for my $channel (keys %{$friends{$mask}{$net}}) { print FILE "$mask\t$net\t$channel\t". join("", sort map {$flagshort{$_}} keys %{$friends{$mask}{$net}{$channel}}). "\n"; } } } close FILE; crap("Saved $count friends to $file") unless $auto; } sub is_friends_window { my($win) = @_; return $win->{name} eq ''; } sub get_friends_window { my($win) = Irssi::window_find_name(''); if ($win) { $win->set_active; } else { Irssi::command("window new hide"); $win = Irssi::active_win; $win->set_name(''); $win->set_history(''); } return $win; } sub get_friend { my($channel,$nick) = @_; my($server) = $channel->{server}; my($chan) = lc $channel->{name}; my($net) = lc $server->{chatnet}; my($flags,@friend); for my $mask (keys %friends) { next unless $server->mask_match_address($mask, $nick->{nick}, $nick->{host}); for my $n ('*', $net) { for my $c ('*', $chan) { if (exists $friends{$mask}{$n}{$c}) { for my $flag (keys %{$friends{$mask}{$n}{$c}}) { $flags->{$flag} = 1; } } } } return $flags if $flags; } return undef; } sub check_friends { my($channel, @nicks) = @_; my(%op,%voice); my($nick,$friend,$list); my(@friends); return unless $channel->{chanop} || $channel->{ownnick}{op}; for $nick (@nicks) { $friend = get_friend($channel, $nick); next unless $friend; next if $nick->{nick} eq $channel->{server}{nick}; if ($friend->{op} && !$nick->{op}) { $op{$nick->{nick}} = 1; } if ($friend->{voice} && !$nick->{voice}) { $voice{$nick->{nick}} = 1; } push @friends, ($nick->{op}?'@':''). ($nick->{voice}?'+':'').$nick->{nick}; } if (@friends && Irssi::settings_get_bool("friends_show_check")) { my($max) = Irssi::settings_get_int("friends_max_nicks"); @friends = sort @friends; $channel->printformat(MSGLEVEL_CLIENTCRAP, @friends>$max ? 'friends_check_more' : 'friends_check', join(" ", splice @friends, 0, $max), scalar @friends); } if ($list = join " ", sort keys %op) { $channel->command("op $list"); } if ($list = join " ", sort keys %voice) { $channel->command("voice $list"); } } sub update_friends_hash { %friends = (); for (@friends) { my($num,$mask,$chan,$net,$flags) = @$_; for (split //, $flags) { $friends{$mask}{$net}{$chan}{$flaglong{$_}} = 1; } } } sub update_friends_window { my($win) = Irssi::window_find_name(''); my($view); my($num) = 0; my($mask,$net,$channel,$flags); my(%net); if ($win) { @friends = (); for $mask (sort keys %friends) { for $net (sort keys %{$friends{$mask}}) { for $channel (sort keys %{$friends{$mask}{$net}}) { $flags = join "", sort map {$flagshort{$_}} keys %{$friends{$mask}{$net}{$channel}}; push @friends, [ ++$num, $mask, $channel, $net, $flags ]; } } } $view = $win->view; $view->remove_all_lines(); $view->clear(); $win->printformat(MSGLEVEL_NEVER, 'friends_header', '##', 'Mask', 'Channel', 'ChatNet', 'Flags'); for (@friends) { ($num,$mask,$channel,$net,$flags) = @$_; if (!$net{$net}) { my($n) = Irssi::chatnet_find($net); $net{$net} = $n?$n->{name}:$net; } $win->printformat(MSGLEVEL_NEVER, 'friends_line', $num, $mask, $channel, $net{$net}, $flags); } $win->printformat(MSGLEVEL_NEVER, 'friends_footer', scalar @friends); } } sub sig_send_command { my($win) = Irssi::active_win; if (is_friends_window($win)) { my($cmd,@param) = split " ", $_[0]; my($changed) = 0; Irssi::signal_stop; for (lc $cmd) { s,^/,,; if (/^m(ask)?$/) { $changed = subcmd_friends_mask($win,@param); } elsif (/^c(han(nel)?)?$/) { $changed = subcmd_friends_channel($win,@param); } elsif (/^(?:n(et)?|chat(net)?)$/) { $changed = subcmd_friends_net($win,@param); } elsif (/^del(ete)?$/) { $changed = subcmd_friends_delete($win,@param); } elsif (/^f(lags?)?$/) { $changed = subcmd_friends_flags($win,@param); } elsif (/^s(ave)?/) { save_friends(); } elsif (/^(?:e(xit)?|q(uit)?)$/) { $win->destroy; } elsif (/^(?:h(elp)?|\?)$/) { subcmd_friends_help($win); } else { $win->print("CMD: $cmd @{[map{\"[$_]\"}@param]}"); } } if ($changed) { update_friends_hash(); update_friends_window(); save_friends(1); } } } sub sig_massjoin { my($channel, $nicks) = @_; check_friends($channel, @$nicks); } sub sig_nick_mode_changed { my($channel, $nick) = @_; if ($channel->{synced} && $channel->{server}{nick} eq $nick->{nick}) { check_friends($channel, $channel->nicks); } } sub sig_channel_sync { my($channel) = @_; check_friends($channel, $channel->nicks); } sub sig_setup_reread { load_friends; } sub sig_setup_save { my($mainconf,$auto) = @_; save_friends($auto); } sub sig_window_changed { my($new,$old) = @_; if (is_friends_window($new)) { update_friends_window(); } } sub sig_message_public { my($server, $msg, $nick, $addr, $target) = @_; my($window,$theme,$friend,$oform,$nform); my($channel) = $server->channel_find($target); return unless $channel; my($color) = Irssi::settings_get_str("friends_nick_color"); $friend = get_friend($channel, $channel->nick_find($nick)); if ($friend && $color =~ /^[rgbcmykpwRGBCMYKPWFU0-9_]$/) { $window = $server->window_find_item($target); $theme = $window->{theme} || Irssi::current_theme; $oform = $nform = $theme->get_format('fe-common/core', 'pubmsg'); $nform =~ s/(\$(\[-?\d+\])?0)/%$color$1%n/g; $window->command("^format pubmsg $nform"); Irssi::signal_continue(@_); $window->command("^format pubmsg $oform"); } } sub sig_message_irc_action { my($server, $msg, $nick, $addr, $target) = @_; my($window,$theme,$friend,$oform,$nform); my($channel) = $server->channel_find($target); return unless $channel; my($color) = Irssi::settings_get_str("friends_nick_color"); $friend = get_friend($channel, $channel->nick_find($nick)); if ($friend && $color =~ /^[rgbcmykpwRGBCMYKPWFU0-9_]$/) { $window = $server->window_find_item($target); $theme = $window->{theme} || Irssi::current_theme; $oform = $nform = $theme->get_format('fe-common/irc', 'action_public'); $nform =~ s/(\$(\[-?\d+\])?0)/%$color$1%n/g; $window->command("^format action_public $nform"); Irssi::signal_continue(@_); $window->command("^format action_public $oform"); } } # Usage: /FRIENDS sub cmd_friends { my($win) = get_friends_window; update_friends_window(); } sub subcmd_friends_channel { my($win,$num,$chan) = @_; unless ($chan && defined $num) { $win->print("Syntax: CHANNEL ", MSGLEVEL_NEVER); return; } unless (0 < $num && $num <= @friends) { $win->print("Error: Element $num not in list", MSGLEVEL_NEVER); return; } $friends[$num-1][2] = $chan; return 1; } sub subcmd_friends_delete { my($win,$num) = @_; unless (defined $num) { $win->print("Syntax: DELETE ", MSGLEVEL_NEVER); return; } unless (0 < $num && $num <= @friends) { $win->print("Error: Element $num not in list", MSGLEVEL_NEVER); return; } splice @friends, $num-1, 1; return 1; } sub subcmd_friends_flags { my($win,$num,$flags) = @_; my(%f); unless ($flags && defined $num) { $win->print("Syntax: FLAGS ", MSGLEVEL_NEVER); return; } unless (0 < $num && $num <= @friends) { $win->print("Error: Element $num not in list", MSGLEVEL_NEVER); return; } $friends[$num-1][4] = join "", sort grep {!$f{$_}++} split //, $flags; return 1; } sub subcmd_friends_help { my($win) = @_; $win->print(q{CHANNEL - set channel is either a channel name or * for all }, MSGLEVEL_NEVER); $win->print(q{DELETE - delete entry }, MSGLEVEL_NEVER); $win->print(q{FLAGS - set flags is a list of c (color), o (give op), v (give voice) }, MSGLEVEL_NEVER); $win->print(q{MASK - set mask is in the usual nick!user@host format }, MSGLEVEL_NEVER); $win->print(q{NET - set net is one of your defined ircnets or * for all }, MSGLEVEL_NEVER); } sub subcmd_friends_mask { my($win, $num, $mask) = @_; unless ($mask && defined $num) { $win->print("Syntax: MASK ", MSGLEVEL_NEVER); return; } unless (0 < $num && $num <= @friends) { $win->print("Error: Element $num not in list", MSGLEVEL_NEVER); return; } unless ($mask =~ /^.+!.+@.+$/) { $win->print("Error: Mask $mask is not valid", MSGLEVEL_NEVER); } $friends[$num-1][1] = $mask; return 1; } sub subcmd_friends_net { my($win,$num,$net) = @_; my($n); unless ($net && defined $num) { $win->print("Syntax: NET ", MSGLEVEL_NEVER); return; } unless (0 < $num && $num <= @friends) { $win->print("Error: Element $num not in list", MSGLEVEL_NEVER); return; } if ($net eq '*') { # all is well } elsif ($n = Irssi::chatnet_find($net)) { $net = $n->{name}; } else { $win->print("Error: No defined chatnet named $net", MSGLEVEL_NEVER); return; } $friends[$num-1][3] = $net; return 1; } # Usage: /ADDFRIEND | [|* [|*]] # [-mask host|normal|domain|full] # [-flags ] sub cmd_addfriend { my($param,$serv,$chan) = @_; my(@param,@flags); my($type) = Irssi::Irc::MASK_USER | Irssi::Irc::MASK_DOMAIN; my($mask,$flags,$channel,$net); my(@split) = split " ", $param; while (@split) { $_ = shift @split; if (/^-m(ask)?$/) { $_ = shift @split; if (/^h(ost)?$/) { $type = Irssi::Irc::MASK_HOST; } elsif (/^n(ormal)?$/) { $type = Irssi::Irc::MASK_USER | Irssi::Irc::MASK_DOMAIN; } elsif (/^d(omain)?$/) { $type = Irssi::Irc::MASK_DOMAIN; } elsif (/^f(ull)?$/) { $type = Irssi::Irc::MASK_NICK | Irssi::Irc::MASK_USER | Irssi::Irc::MASK_HOST; } else { # fjekk } } elsif (/^-flags?$/) { $flags = shift @split; } else { push @param, $_; } } ($mask,$channel,$net) = @param; unless ($mask) { crap("/ADDFRIEND [-mask full|normal|host|domain] [-flags <[o][v][c]>] [ []]]"); return; } $flags ||= "o"; unless ($channel) { if ($chan) { $channel = $chan->{name}; } else { crap("/ADDFRIEND needs a channel."); return; } } unless ($net) { if ($serv) { $net = $serv->{chatnet}; } else { crap("/ADDFRIEND needs a chatnet."); return; } } # is this a nick we need to expand? unless ($mask =~ /.+!.+@.+/) { my($nick); if ($net ne '*') { unless ($serv = Irssi::server_find_chatnet($net)) { crap("Error locating server for $net."); return; } } else { unless ($serv) { crap("Need a server for nick expansion"); return } } if ($channel ne '*') { unless ($chan = $serv->channel_find($channel)) { crap("Error locating channel $channel."); return; } } else { unless ($chan) { crap("Need a channel for nick expansion"); return; } } unless ($nick = $chan->nick_find($mask)) { crap("Error locating nick $mask."); return; } $mask = Irssi::Irc::get_mask($nick->{nick}, $nick->{host}, $type); } for my $flag (split //, $flags) { unless ($flag = $flaglong{$flag}) { crap("Unknown flag [$flag]"); next; } push @flags, $flag; $friends{$mask}{lc $net}{lc $channel}{$flag} = 1; } if (@flags) { crap("Added %s for %s in %s on %s.", join(",", @flags), $mask, $channel, $net); } save_friends(1); } Irssi::settings_add_bool('friends', 'friends_autosave', 1); Irssi::settings_add_int('friends', 'friends_max_nicks', 10); Irssi::settings_add_bool('friends', 'friends_show_check', 1); Irssi::settings_add_str('friends', 'friends_nick_color', ''); Irssi::theme_register( [ 'friends_crap', '{line_start}{hilight Friends:} $0', 'friends_check', '{line_start}{hilight Friends} checked: $0', 'friends_check_more', '{line_start}{hilight Friends} checked: $0 (+$1 more)', 'friends_header', '<%W$[2]0%n> <%W$[33]1%n> <%W$[13]2%n> <%W$[13]3%n> <%W$[5]4%n>', 'friends_line', '[%R$[-2]0%n] $[35]1 $[15]2 $[15]3 $[7]4', 'friends_footer', "\n".'%4 List contains $0 friends %>%n', ]); Irssi::signal_add_first("send command", "sig_send_command"); Irssi::signal_add_last("massjoin", "sig_massjoin"); Irssi::signal_add_last("nick mode changed", "sig_nick_mode_changed"); Irssi::signal_add_last("channel sync", "sig_channel_sync"); Irssi::signal_add('setup saved', 'sig_setup_save'); Irssi::signal_add('setup reread', 'sig_setup_reread'); Irssi::signal_add('window changed', 'sig_window_changed'); Irssi::signal_add_first('message public', 'sig_message_public'); Irssi::signal_add_first('message irc action', 'sig_message_irc_action'); Irssi::command_bind('friends', 'cmd_friends'); Irssi::command_bind('addfriend', 'cmd_addfriend'); load_friends;