#
# 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 => '[email protected]',
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;