summaryrefslogtreecommitdiffstats
path: root/scripts/people.pl
diff options
context:
space:
mode:
authorAlexander Færøy2014-05-31 13:10:46 +0200
committerAlexander Færøy2014-05-31 13:10:46 +0200
commit2d0759e6ca5767b48bcc85bf38c2c43d5f0b63b1 (patch)
tree1c5e6d817c88e67b46e216a50e0aef5428bf63df /scripts/people.pl
parent2d080422d79d1fd49d6c5528593ccaaff9bfc583 (diff)
downloadscripts.irssi.org-2d0759e6ca5767b48bcc85bf38c2c43d5f0b63b1.tar.bz2
Import scripts from scripts.irssi.org
Diffstat (limited to 'scripts/people.pl')
-rw-r--r--scripts/people.pl2492
1 files changed, 2492 insertions, 0 deletions
diff --git a/scripts/people.pl b/scripts/people.pl
new file mode 100644
index 0000000..9823207
--- /dev/null
+++ b/scripts/people.pl
@@ -0,0 +1,2492 @@
+use strict;
+use 5.005_62; # for 'our'
+use Irssi 20020428; # for Irssi::signal_continue
+use Time::HiRes;
+use vars qw($VERSION %IRSSI);
+
+our $has_crypt = 0;
+eval {require Crypt::PasswdMD5};
+unless ($@) {
+ $has_crypt = 1;
+ import Crypt::PasswdMD5;
+}
+
+$VERSION = "1.7";
+%IRSSI =
+(
+ authors => "Marcin 'Qrczak' Kowalczyk, Johan 'ion' Kiviniemi",
+ contact => 'qrczak@knm.org.pl',
+ name => 'People',
+ description => 'Userlist with autoopping, autokicking etc.',
+ license => 'GNU GPL',
+ url => 'http://qrnik.knm.org.pl/~qrczak/irc/people.pl',
+ url_ion => 'http://johan.kiviniemi.name/stuff/irssi/people.pl',
+);
+
+######## STATE ########
+
+our %handles;
+our %user_masks;
+our %user_flags;
+our %channel_flags;
+our %user_channel_flags;
+our %authenticated = ();
+our %expire_auth = ();
+
+our $config = Irssi::get_irssi_dir . "/people.cfg";
+our $config_tmp = Irssi::get_irssi_dir . "/people.tmp";
+our $config_old = Irssi::get_irssi_dir . "/people.cfg~";
+
+Irssi::settings_add_bool 'people', 'people_autosave', 1;
+Irssi::settings_add_int 'people', 'people_op_delay_min', 10;
+Irssi::settings_add_int 'people', 'people_op_delay_max', 20;
+Irssi::settings_add_str 'people', 'people_default_chatnet', "DALnet";
+Irssi::settings_add_bool 'people', 'people_color_friends', 0;
+Irssi::settings_add_bool 'people', 'people_color_everybody', 0;
+Irssi::settings_add_int 'people', 'people_expire_password', 60;
+Irssi::settings_add_bool 'people', 'people_channel_notice', 1;
+Irssi::settings_add_str 'people', 'people_colors', "rgybmcRGYBMC";
+
+our $handle_re = qr/([^\0- &#+!,\-\177][^\0- ,\177]*)/;
+our $mask_re = qr/([^\0- \177]+)/;
+our $masks_re = qr/([^\0- \177]+(?: +[^\0- \177]+)*)/;
+our $opt_masks_re = qr/((?: +[^\0- \177]+)*)/;
+our $chatnet_re = qr/([\w-._]+)/;
+our $channel_re = qr/([&#+!][^\0- ,\177]*)/;
+our $channels_re = qr/([&#+!][^\0- ,\177]*(?:,[&#+!][^\0- ,\177]*)*)/;
+our $mask_re = qr/([^\0- \177]+)/;
+our $flags_re = qr/((?:[+\-!][a-zA-Z]+)+)/;
+our $arg_re = qr/(?: (.*))?/;
+our $nick_re = qr/([A-}][\-0-9A-}]*)/;
+our $nicks_re = qr/([A-}][\-0-9A-}]*(?: +[A-}][\-0-9A-}]*)*)/;
+our $nicks_commas_re = qr/([A-}][\-0-9A-}]*(?:,[A-}][\-0-9A-}]*)*)/;
+
+our $master_set_flags = 'deikmopqrvx';
+our $master_see_flags = 'deiklmopqrvx';
+our $all_flags = 'cdeiklmnopqrvx';
+
+sub tr_flag {
+ my ($flag) = @_;
+ $flag =~ tr/CIL/cil/;
+ return $flag;
+}
+
+our %master_set_flags = map {$_ => 1} split //, $master_set_flags;
+our %master_see_flags = map {$_ => 1} split //, $master_see_flags;
+our %all_flags = map {$_ => 1} split //, $all_flags;
+
+######## HELP ########
+
+our $help_commands =
+
+our %help = (
+ people => [
+ 'When I meet people, they are recognized based on their nick and',
+ 'address, and actions can be automatically performed upon them',
+ '(such as opping or kicking).',
+ '',
+ 'Actions depend on flags associated with the user in the channel.',
+ 'Flags can be specified globally for a user, for everybody in',
+ 'a channel, or locally for a user in a channel. A flag setting',
+ 'can be positive or negative. If conflicting settings are present',
+ 'for a flag, local setting is more important than channel setting',
+ 'which is more important than global setting.',
+ '',
+ 'A user handle has a set of nick & address masks used to recognize',
+ 'that person. If someone matches masks of several users, all their',
+ 'flags are considered together, resolving conflicts in favor of',
+ 'more specific masks.',
+ '',
+ 'Commands which modify the user list may be given locally',
+ 'by the owner of the script (e.g. /flag someone +o) or',
+ 'remotely by someone with enough privileges, either by msg',
+ '(e.g. /msg Qrczak !flag someone +o), or ctcp',
+ '(e.g. /ctcp Qrczak flag someone +o).',
+ '',
+ 'Commands which manage the user list can be used only by people',
+ 'with the master status (+m). A local master can manage only',
+ 'local users (+l) who don\'t have any flags outside his channels.',
+ 'Commands which perform actions in channels can be used only',
+ 'by people with the operator status (+o).',
+ '',
+ 'You can use "help <command>" to learn details about the command.',
+ 'Available commands: help, user add, user remove, mask add,',
+ 'mask remove, user rename, user list, flag, find, trust, op, deop,',
+ 'voice, devoice, kick, ban, unban, kickban, invite.',
+ ],
+ help => [
+ 'HELP [<command>]',
+ '',
+ 'Show details about the command, or introduction to the script',
+ 'if no argument is given.',
+ ],
+ 'user add' => [
+ 'USER ADD <handle> <mask>...',
+ '',
+ 'Add a user, recognized by address masks (nick!user@host or',
+ 'user@host or host). <handle> is a user name for internal use by',
+ 'the script. If <masks> are omitted and a user with nick <handle>',
+ 'is on a channel with the owner of the script, try to guess the',
+ 'mask basing on his address: replace the first part of host with *',
+ 'if it contains any digits, or replace the last part of IP address',
+ 'with * if the address is a numeric IP. You must be a master (+m)',
+ 'somewhere to use this command.',
+ ],
+ 'user remove' => [
+ 'USER REMOVE <handle>',
+ '',
+ 'Remove all information about the user <handle>.',
+ ],
+ 'mask add' => [
+ 'MASK ADD <handle> <mask>...',
+ '',
+ 'Add more address masks to recognize user <handle>.',
+ ],
+ 'mask remove' => [
+ 'MASK REMOVE <handle> <mask>...',
+ '',
+ 'Remove some address masks used to recognize user <handle>.',
+ ],
+ 'user rename' => [
+ 'USER RENAME <handle> <new-handle>',
+ '',
+ 'Use a new internal name <new-handle> for the user <handle>.',
+ ],
+ 'user list' => [
+ 'USER LIST [[<chatnet>/]<#channels>] [+<flags>]',
+ 'USER LIST text...',
+ '',
+ 'List all users, or users having any flags in the specified',
+ 'channels, or users having any of the specified flags somewhere,',
+ 'or users having any of the specified flags in the channels,',
+ 'or users having any of the specified texts in handle, address',
+ 'masks or flag arguments.',
+ ],
+ flag => [
+ 'FLAG <handle>',
+ 'FLAG [<chatnet>/]<#channels>',
+ 'FLAG <handle> <flags>',
+ 'FLAG [<chatnet>/]<#channels> <flags>',
+ 'FLAG <handle> [<chatnet>/]<#channels> <flags>',
+ '',
+ 'Without flags given, show flags of the user or channel.',
+ 'Otherwise add or remove flags globally for a user, for',
+ 'everybody in a channel, or locally for a user in a channel.',
+ '',
+ '<flags> is +<letters> (add these flags), -<letters> (remove',
+ 'these flags, or set them as a negative exception if the flag',
+ 'would othwerise come from global or channel setting), !<letters>',
+ '(set these flags as a negative exception) or a combination of',
+ 'such settings. If the last flag is being added, it may be followed',
+ 'by space and <argument> for that flag whose meaning depends on',
+ 'the flag.',
+ '',
+ 'Meanings of flags:',
+ '',
+ '+c - Color nick on public messages. This flag is meaningful',
+ ' only for the owner of the script. The color will be',
+ ' computed from the handle. If people_color_friends variable',
+ ' is set, nicks of all recognized people will be colored.',
+ ' If people_color_everybody variable is set, every nick',
+ ' will be colored, basing on the nick if the person is not',
+ ' recognized. The color may be also specified explicitly in',
+ ' the argument of +c:',
+ ' %k - black, %r - red, %g - green, %y - yellow or brown,',
+ ' %b - blue, %m - magenta, %c - cyan, %w - white,',
+ ' %K %R %G %Y %B %M %C %W - bright variants of these colors.',
+ '',
+ '+d - Deop if he gets op, except when opped by you or by a',
+ ' master (+m). When flags conflict, +o and +r override +d.',
+ '',
+ '+e - Execute command given as the argument. $C is replaced with',
+ ' the channel the person entered, $N - nick, $A - address.',
+ '',
+ '+i - A comment or information which reminds why the person is',
+ ' interesting can be stored in the argument of +i. It has',
+ ' no real effect. It\'s only shown with notification (+n).',
+ '',
+ '+k - Ban and kick out. The ban mask will be the mask used to',
+ ' recognize him, or based on his address if +k came from',
+ ' channel flags (replace the first part of host with * if it',
+ ' contains any digits, or replace the last part of IP address',
+ ' with * if the address is a numeric IP). The kick reason may',
+ ' be specified in the argument of the +k flag. When flags',
+ ' conflict, +o and +r override +k.',
+ '',
+ '+l - Local user. Can have address masks changed by a local master',
+ ' if the user doesn\'t have any flags outside the master\'s',
+ ' channels.',
+ '',
+ '+m - Master. Can manage the user list, or a local part of it if',
+ ' only a local master. His actions on other users (opping and',
+ ' deopping) will not be questioned by +r and +d of these users.',
+ '',
+ '+n - Notify you when the user joins or leaves channels. This flag',
+ ' is meaningful only for the owner of the script.',
+ '',
+ '+o - Op, after a short random delay to avoid op flood when he',
+ ' would be opped by others anyway.',
+ '',
+ '+p - Password is needed to recognize that person. This flag',
+ ' should be used when address masks are not secure, i.e.',
+ ' unwanted people can have the same addresses. When +p has',
+ ' no argument, the person doesn\'t have the password set',
+ ' yet and should use the PASS command to set it. Once set,',
+ ' the password is stored encrypted in the argument of +p',
+ ' and the person must use the PASS command to be recognized.',
+ ' The people_expire_password variable tells how many seconds',
+ ' to remember the authorization if the person is not seen',
+ ' on any channels.',
+ '',
+ '+q - Devoice if he gets voiced, except when voiced by you or',
+ ' by a master (+m).',
+ '',
+ '+r - Reop if somebody deops him, except when deopped by you,',
+ ' by himself, or by a master (+m).',
+ '',
+ '+v - Voice, after a short random delay to avoid voice flood',
+ ' when he would be voiced or opped by others anyway.',
+ '',
+ '+x - Disable all other flags, except perhaps notification (+n).',
+ ],
+ find => [
+ 'FIND',
+ 'FIND [<chatnet>/]<#channel>',
+ 'FIND <mask>',
+ 'FIND <nick>',
+ '',
+ 'Find recognized users on all channels (only owner can do this),',
+ 'or on the channel, or matching the mask, or having the nick if',
+ 'present on a channel with me.',
+ ],
+ trust => [
+ 'TRUST [<nick>]...',
+ '',
+ 'Set these nicks as authenticated.',
+ ],
+ op => [
+ 'OP <#channel> [<nick>]...',
+ '',
+ 'Op these nicks in the channel. If nicks are not given, ops you.',
+ ],
+ deop => [
+ 'DEOP <#channel> [<nick>]...',
+ '',
+ 'Deop these nicks in the channel. If nicks are not given,',
+ 'deops you.',
+ ],
+ voice => [
+ 'VOICE <#channel> [<nick>]...',
+ '',
+ 'Voices these nicks in the channel. If nicks are not given,',
+ 'voices you.',
+ ],
+ devoice => [
+ 'DEVOICE <#channel> [<nick>]...',
+ '',
+ 'Devoices these nicks in the channel. If nicks are not given,',
+ 'devoices you.',
+ ],
+ kick => [
+ 'KICK <#channel> <nicks> [<reason>]',
+ '',
+ 'Kick these nicks out of the channel.',
+ ],
+ ban => [
+ 'BAN <#channel> <mask/nick>...',
+ '',
+ 'Ban address masks from the channel. If a nick of a person',
+ 'sitting there is given, the mask is derived from his address.',
+ ],
+ unban => [
+ 'UNBAN <#channel> [<masks>]',
+ '',
+ 'Remove some bans from the channel. If no masks are given,',
+ 'remove all bans against you.',
+
+ ],
+ kickban => [
+ 'KICKBAN <#channel> <nicks> [<reason>]',
+ '',
+ 'Ban and kick out people from the channel. The mask to ban',
+ 'is derived from their addresses.',
+ ],
+ invite => [
+ 'INVITE <#channel> [<nick>]',
+ '',
+ 'Invite the person to the channel. If the nick is not given,',
+ 'invite you.',
+ ],
+ pass => [
+ 'PASS <password>',
+ 'PASS <password> <new-password>',
+ '',
+ 'Authenticate with the password to ensure the owner that you',
+ 'are the right person (if you have the +p flag), or set the',
+ 'password if it wasn\'t set yet. To change the password once',
+ 'it was set, give both old and new passwords.',
+ ]
+);
+
+our %local_help = (people => 1);
+
+sub cmd_help($$) {
+ my ($context, $args) = @_;
+ my $command = join(' ', split(' ', lc $args));
+ $command = 'people' if !$context->{owner} && $command eq '';
+ my $text = $help{$command};
+ if (!$text || $context->{owner} && !$local_help{$command}) {
+ $context->{error}("No help for $command") unless $context->{owner};
+ return;
+ }
+ foreach my $line ('', @$text, '') {
+ $context->{crap}($line eq '' ? ' ' : $line);
+ }
+ Irssi::signal_stop if $context->{owner};
+}
+
+######## A REGEXP OF ALL MASKS TO IMPROVE PERFORMANCE ########
+
+our %mask_to_regexp = ();
+foreach my $i (0..255) {
+ my $ch = chr $i;
+ $mask_to_regexp{$ch} = "\Q$ch\E";
+}
+$mask_to_regexp{'?'} = '.';
+$mask_to_regexp{'*'} = '.*';
+
+sub mask_to_regexp($) {
+ my ($mask) = @_;
+ $mask =~ s/(.)/$mask_to_regexp{$1}/g;
+ return $mask;
+}
+
+our $all_masks;
+
+sub update_all_masks() {
+ my @masks = ();
+ foreach my $hdl (keys %handles) {
+ push @masks, @{$user_masks{$hdl}};
+ }
+ $all_masks = join('|', map {mask_to_regexp $_} @masks);
+ $all_masks = qr/^(?:$all_masks)$/i;
+}
+
+######## CONTEXT OF COMMANDS: LOCAL OR REPLYING TO MESSAGES ########
+
+our $local_context = {
+ crap => sub {my ($msg) = @_; $msg =~ s/%/%%/g; print CLIENTCRAP $msg},
+ notice => sub {my ($msg) = @_; $msg =~ s/%/%%/g; print CLIENTNOTICE $msg},
+ error => sub {my ($msg) = @_; $msg =~ s/%/%%/g; print CLIENTERROR $msg},
+ usage => sub {my ($msg) = @_; $msg =~ s/%/%%/g; print CLIENTERROR "Usage: /$msg"},
+ usage_next => sub {my ($msg) = @_; $msg =~ s/%/%%/g; print CLIENTERROR " /$msg"},
+ owner => 1,
+ set_flags => \%all_flags,
+ set_flags_str => $all_flags,
+ see_flags => \%all_flags,
+ server => undef,
+};
+
+######## CHECK PRIVILEGES TO PERFORM COMMANDS ########
+
+sub has_global_flag($$) {
+ my ($context, $flag) = @_;
+ return $context->{owner} || defined $context->{globals}{$flag};
+}
+
+sub has_local_flag($$$$) {
+ my ($context, $chatnet, $channel, $flag) = @_;
+ return 1 if $context->{owner};
+ return
+ exists $context->{locals}{$chatnet}{$channel}{$flag} ?
+ defined $context->{locals}{$chatnet}{$channel}{$flag} :
+ exists $channel_flags{$chatnet}{$channel}{$flag} ?
+ defined $channel_flags{$chatnet}{$channel}{$flag} :
+ defined $context->{globals}{$flag};
+}
+
+sub has_flag_somewhere($$) {
+ my ($context, $flag) = @_;
+ return 1 if $context->{owner} || defined $context->{globals}{$flag};
+ my $locals = $context->{locals};
+ foreach my $chatnet (keys %$locals) {
+ my $channels = $locals->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ my $flags = $channels->{$channel};
+ return 1 if defined $flags->{$flag};
+ }
+ }
+ return 0;
+}
+
+sub must_be_master($) {
+ my ($context) = @_;
+ return 1 if has_flag_somewhere($context, 'm');
+ $context->{error}("Sorry, you don't have master privileges.");
+ return 0;
+}
+
+sub must_be_operator($) {
+ my ($context) = @_;
+ return 1 if has_flag_somewhere($context, 'o') ||
+ has_flag_somewhere($context, 'm');
+ $context->{error}("Sorry, you don't have operator privileges.");
+ return 0;
+}
+
+sub may_manage($$) {
+ my ($context, $hdl) = @_;
+ return 1 if has_global_flag($context, 'm');
+ unless (defined $user_flags{$hdl}{l}) {
+ $context->{error}("Sorry, \cc04$handles{$hdl}\co isn't local to your channels.");
+ return 0;
+ }
+ my $locals = $user_channel_flags{$hdl};
+ foreach my $chatnet (keys %$locals) {
+ my $channels = $locals->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ my $flags = $channels->{$channel};
+ foreach my $flag (keys %$flags) {
+ next unless defined $flags->{$flag};
+ unless (defined $context->{locals}{$chatnet}{$channel}{m}) {
+ $context->{error}("Sorry, \cc04$handles{$hdl}\co has flags outside your channels.");
+ return 0;
+ }
+ }
+ }
+ }
+ return 1;
+}
+
+######## FIND USERS AND FLAGS ########
+
+sub more_specific($$) {
+ my ($user1, $user2) = @_;
+ return 0 unless $user1 && $user2;
+ my $mask1 = $user1->[1];
+ my $mask2 = $user2->[1];
+ return 0 if $mask1 eq $mask2;
+ $mask1 =~ /^(.*)!(.*)$/ or return 0;
+ my ($nick1, $address1) = ($1, $2);
+ $mask2 =~ /^(.*)!(.*)$/ or return 0;
+ my ($nick2, $address2) = ($1, $2);
+ return 0 if Irssi::mask_match_address($mask1, $nick2, $address2);
+ return 1 if Irssi::mask_match_address($mask2, $nick1, $address1);
+ return 0 if Irssi::mask_match_address($address1, $address2, undef);
+ return 1 if Irssi::mask_match_address($address2, $address1, undef);
+ $address1 =~ s/^.*\@/*\@/;
+ $address2 =~ s/^.*\@/*\@/;
+ return 0 if Irssi::mask_match_address($address1, $address2, undef);
+ return 1 if Irssi::mask_match_address($address2, $address1, undef);
+ return 0;
+}
+
+sub find_users($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ return () unless "$nick!$address" =~ $all_masks;
+ my @users = ();
+ foreach my $hdl (keys %user_masks) {
+ next if defined $chatnet &&
+ defined $user_flags{$hdl}{p} &&
+ !$authenticated{$chatnet}{$address}{$hdl};
+ my $masks = $user_masks{$hdl};
+ foreach my $mask (@$masks) {
+ if (Irssi::mask_match_address($mask, $nick, $address)) {
+ push @users, [$hdl, $mask];
+ }
+ }
+ }
+ return @users;
+}
+
+sub find_best_user($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ my $best = undef;
+ foreach my $user (find_users $chatnet, $nick, $address) {
+ $best = $user unless more_specific($best, $user);
+ }
+ return $best ? @$best : ();
+}
+
+sub add_flag($$$$$) {
+ my ($flags, $users, $flag, $arg, $user) = @_;
+ return if
+ exists $flags->{$flag} &&
+ more_specific($users->{$flag}, $user);
+ $flags->{$flag} = $arg;
+ $users->{$flag} = $user;
+}
+
+sub find_global_flags($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ my $flags = {}; my $users = {};
+ foreach my $user (find_users $chatnet, $nick, $address) {
+ my ($hdl, $mask) = @$user;
+ my $globals = $user_flags{$hdl};
+ foreach my $flag (keys %$globals) {
+ my $arg = $globals->{$flag};
+ add_flag $flags, $users, $flag, $arg, $user;
+ }
+ add_flag $flags, $users, '', '', $user;
+ }
+ return ($flags, $users);
+}
+
+sub find_local_flags($$$$) {
+ my ($chatnet, $channel, $nick, $address) = @_;
+ my @users = find_users $chatnet, $nick, $address;
+ my $flags = {}; my $users = {};
+ foreach my $user (@users) {
+ my ($hdl, $mask) = @$user;
+ my $globals = $user_flags{$hdl};
+ foreach my $flag (keys %$globals) {
+ my $arg = $globals->{$flag};
+ add_flag $flags, $users, $flag, $arg, $user;
+ }
+ add_flag $flags, $users, '', '', $user;
+ }
+ my $chan_flags = $channel_flags{$chatnet}{$channel};
+ foreach my $flag (keys %$chan_flags) {
+ my $arg = $chan_flags->{$flag};
+ add_flag $flags, $users, $flag, $arg, undef;
+ }
+ foreach my $user (@users) {
+ my ($hdl, $mask) = @$user;
+ my $locals = $user_channel_flags{$hdl}{$chatnet}{$channel};
+ foreach my $flag (keys %$locals) {
+ my $arg = $locals->{$flag};
+ add_flag $flags, $users, $flag, $arg, $user;
+ }
+ }
+ return ($flags, $users);
+}
+
+sub find_local_flags_if_matches($$$$$) {
+ my ($hdl, $chatnet, $channel, $nick, $address) = @_;
+ my $user = undef;
+ foreach my $mask (@{$user_masks{$hdl}}) {
+ if (Irssi::mask_match_address($mask, $nick, $address)) {
+ $user = [$hdl, $mask]; last;
+ }
+ }
+ return ({}, {}) unless $user;
+ my $flags = {}; my $users = {};
+ my $globals = $user_flags{$hdl};
+ foreach my $flag (keys %$globals) {
+ my $arg = $globals->{$flag};
+ add_flag $flags, $users, $flag, $arg, $user;
+ }
+ add_flag $flags, $users, '', '', $user;
+ my $chan_flags = $channel_flags{$chatnet}{$channel};
+ foreach my $flag (keys %$chan_flags) {
+ my $arg = $chan_flags->{$flag};
+ add_flag $flags, $users, $flag, $arg, undef;
+ }
+ my $locals = $user_channel_flags{$hdl}{$chatnet}{$channel};
+ foreach my $flag (keys %$locals) {
+ my $arg = $locals->{$flag};
+ add_flag $flags, $users, $flag, $arg, $user;
+ }
+ return ($flags, $users);
+}
+
+sub find_all_flags($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ my $globals = {}; my $global_users = {};
+ my $locals = {}; my $local_users = {};
+ foreach my $user (find_users $chatnet, $nick, $address) {
+ my ($hdl, $mask) = @$user;
+ my $flags = $user_flags{$hdl};
+ foreach my $flag (keys %$flags) {
+ my $arg = $flags->{$flag};
+ add_flag $globals, $global_users, $flag, $arg, $user;
+ }
+ my $chatnets = $user_channel_flags{$hdl};
+ foreach my $chatnet (keys %$chatnets) {
+ my $channels = $chatnets->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ my $flags = $channels->{$channel};
+ foreach my $flag (keys %$flags) {
+ my $arg = $flags->{$flag};
+ add_flag
+ \%{$locals->{$chatnet}{$channel}},
+ \%{$local_users->{$chatnet}{$channel}},
+ $flag, $arg, $user;
+ }
+ }
+ }
+ }
+ return ($globals, $locals);
+}
+
+######## SHOW USERLIST ########
+
+sub handle_exists($$) {
+ my ($context, $handle) = @_;
+ unless (defined $handles{lc $handle}) {
+ $context->{error}("User \cc04$handle\co doesn't exist.");
+ return 0;
+ }
+ return 1;
+}
+
+sub filter_flags($$) {
+ my ($flags, $filter) = @_;
+ my %filtered = ();
+ foreach my $flag (keys %$flags) {
+ $filtered{$flag} = $flags->{$flag} if $filter->{$flag};
+ }
+ return \%filtered;
+}
+
+sub show_flags($) {
+ my ($flags) = @_;
+ return "(none)" unless $flags && %$flags;
+ my @on = ();
+ my @off = ();
+ foreach my $flag (sort keys %$flags) {
+ push @{defined $flags->{$flag} ? \@on : \@off}, $flag;
+ }
+ return
+ "\cc9" .
+ (@off ? "-" . join('', @off) : '') .
+ (@on ? '+' .
+ join('', grep {$flags->{$_} eq ''} @on) .
+ join('', map {"$_\cc3($flags->{$_})\cc9"} grep {$flags->{$_} ne ''} @on) :
+ '') .
+ "\co";
+}
+
+sub show_handle($$) {
+ my ($context, $hdl) = @_;
+ handle_exists $context, $hdl or return;
+ my $globals = $user_flags{$hdl} || {};
+ $globals = filter_flags $globals, $context->{see_flags}
+ unless $context->{owner};
+ my @locals = ();
+ my $chatnets = $user_channel_flags{$hdl};
+ foreach my $chatnet (sort keys %$chatnets) {
+ my $channels = $chatnets->{$chatnet};
+ foreach my $channel (sort keys %$channels) {
+ my $flags = $channels->{$channel} || {};
+ $flags = filter_flags $flags, $context->{see_flags}
+ unless $context->{owner};
+ push @locals, [$chatnet, $channel, $flags] if %$flags;
+ }
+ }
+ my @masks = @{$user_masks{$hdl}};
+ if (@masks) {
+ my $plural = @masks == 1 ? "" : "s";
+ $context->{crap}("\cc04$handles{$hdl}\co is \cc10@masks\co");
+ } else {
+ $context->{crap}("\cc04$handles{$hdl}\co exists but has no address masks");
+ }
+ my @flags = %$globals ? (show_flags($globals)) : ();
+ foreach my $local (@locals) {
+ my ($chatnet, $channel, $flags) = @$local;
+ push @flags, "\cb$chatnet/$channel\cb " . show_flags($flags)
+ if has_local_flag($context, $chatnet, $channel, 'm');
+ }
+ @flags = ("(none)") unless @flags;
+ $context->{crap}(" flags: " . join("; ", @flags));
+}
+
+sub show_channel($$$$) {
+ my ($context, $chatnet, $channel, $show_empty) = @_;
+ my $flags = $channel_flags{$chatnet}{$channel} || {};
+ $flags = filter_flags $flags, $context->{see_flags}
+ unless $context->{owner};
+ return unless $show_empty || %$flags;
+ $context->{crap}("Flags of \cb$chatnet/$channel\cb are " . show_flags($flags));
+}
+
+sub filter_handle($$$$$) {
+ my ($context, $hdl,
+ $filter_channels, $filter_flags, $filter_text) = @_;
+ return 1 unless $filter_channels || $filter_flags || $filter_text;
+ my $globals = $user_flags{$hdl};
+ my $locals = $user_channel_flags{$hdl};
+ if ($filter_text) {
+ foreach my $re (@$filter_text) {
+ return 1 if $hdl =~ $re;
+ my $masks = $user_masks{$hdl};
+ foreach my $mask (@$masks) {
+ return 1 if $mask =~ $re;
+ }
+ foreach my $flag (keys %$globals) {
+ return 1 if $globals->{$flag} =~ $re;
+ }
+ foreach my $chatnet (keys %$locals) {
+ my $channels = $locals->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ my $flags = $channels->{$channel};
+ foreach my $flag (keys %$flags) {
+ return 1 if defined $flags->{$flag} && $flags->{$flag} =~ $re;
+ }
+ }
+ }
+ }
+ return 0;
+ }
+ if ($filter_flags) {
+ foreach my $flag (@$filter_flags) {
+ next unless $context->{owner} || $context->{see_flags}{$flag};
+ return 1 if defined $globals->{$flag};
+ foreach my $chatnet (keys %$locals) {
+ my $channels = $locals->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ next unless has_local_flag($context, $chatnet, $channel, 'm') &&
+ (!$filter_channels || $filter_channels->{$chatnet}{$channel});
+ my $flags = $channels->{$channel};
+ return 1 if exists $flags->{$flag};
+ }
+ }
+ }
+ return 0;
+ } else {
+ return 1 if $globals && %$globals;
+ foreach my $chatnet (keys %$locals) {
+ my $channels = $locals->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ next unless has_local_flag($context, $chatnet, $channel, 'm') &&
+ $filter_channels->{$chatnet}{$channel};
+ my $flags = $channels->{$channel};
+ return 1 if %$flags;
+ }
+ }
+ return 0;
+ }
+}
+
+sub filter_channel($$$$$$) {
+ my ($context, $chatnet, $channel,
+ $filter_channels, $filter_flags, $filter_text) = @_;
+ return 0 unless has_local_flag($context, $chatnet, $channel, 'm');
+ if ($filter_text) {
+ my $flags = $channel_flags{$chatnet}{$channel};
+ foreach my $re (@$filter_text) {
+ return 1 if $channel =~ $re;
+ foreach my $flag (keys %$flags) {
+ return 1 if $flags->{$flag} =~ $re;
+ }
+ }
+ return 0;
+ }
+ return 0 if $filter_channels && !$filter_channels->{$chatnet}{$channel};
+ return 1 unless $filter_flags;
+ my $flags = $channel_flags{$chatnet}{$channel};
+ foreach my $flag (@$filter_flags) {
+ next unless $context->{owner} || $context->{see_flags}{$flag};
+ return 1 if defined $flags->{$flag};
+ }
+ return 0;
+}
+
+sub default_chatnet($) {
+ my ($context) = @_;
+ my $server = $context->{server} || $context->{owner} && Irssi::active_server;
+ return $server->{chatnet} if $server;
+ return Irssi::settings_get_str('people_default_chatnet');
+}
+
+sub cmd_user_list($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ my $filter_channels = undef;
+ my $filter_flags = undef;
+ my $filter_text = undef;
+ if ($args =~ /^ *(?:(?:$chatnet_re\/)?$channels_re +)?\+([a-zA-Z]+) *$/o ||
+ $args =~ /^ *(?:$chatnet_re\/)?$channels_re *$/o ||
+ $args =~ /^ *$/) {
+ my ($chatnet, $channels, $flags) = ($1, $2, $3);
+ if (defined $channels) {
+ $chatnet = default_chatnet $context unless defined $chatnet;
+ $chatnet = lc $chatnet;
+ $channels = lc $channels;
+ $filter_channels = {$chatnet => {map {$_ => 1} split /,/, $channels}};
+ }
+ $filter_flags = [split //, $flags] if defined $flags;
+ $context->{crap}(
+ $filter_flags ?
+ "Users having " .
+ (length $flags == 1 ? "\cc9+$flags\co flag" : "any of \cc9+$flags\co flags") .
+ ($filter_channels ? " on \cb$chatnet/$channels\cb:" : ":") :
+ $filter_channels ?
+ "Users having any flags on \cb$chatnet/$channels\cb:" :
+ "User list:");
+ } else {
+ my @texts = split ' ', $args;
+ $context->{crap}("Users having something common with \cb@texts\cb:");
+ $filter_text = [map {qr/\Q$_\E/i} @texts];
+ }
+ foreach my $hdl (sort keys %handles) {
+ show_handle $context, $hdl
+ if filter_handle $context, $hdl,
+ $filter_channels, $filter_flags, $filter_text;
+ }
+ foreach my $chatnet (sort keys %channel_flags) {
+ my $channels = $channel_flags{$chatnet};
+ foreach my $channel (sort keys %$channels) {
+ show_channel $context, $chatnet, $channel, 0
+ if filter_channel $context, $chatnet, $channel,
+ $filter_channels, $filter_flags, $filter_text;
+ }
+ }
+ $context->{crap}("End of user list");
+}
+
+######## WORK WHEN MEETING PEOPLE ########
+
+sub channel_notice($$$) {
+ my ($server, $channel, $msg) = @_;
+ $server->command("notice $channel -!- $msg")
+ if Irssi::settings_get_bool('people_channel_notice');
+}
+
+sub disappeared($) {
+ my ($chatnet, $nick, $address, $hdl) = @{$_[0]};
+ delete $authenticated{$chatnet}{$address}{$hdl};
+ delete $authenticated{$chatnet}{$address} unless %{$authenticated{$chatnet}{$address}};
+ delete $expire_auth{$chatnet}{$address}{$hdl};
+ delete $expire_auth{$chatnet}{$address} unless %{$expire_auth{$chatnet}{$address}};
+ print CLIENTNOTICE "\cc11*!$address\co is no longer recognized as \cc04$handles{$hdl}\co (authentication expired).";
+}
+
+sub disappears($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ my $handles = $authenticated{$chatnet}{$address} or return;
+ my $delay = Irssi::settings_get_int('people_expire_password') * 1000;
+ foreach my $hdl (keys %$handles) {
+ my $expiring = $expire_auth{$chatnet}{$address}{$hdl};
+ Irssi::timeout_remove $expiring if $expiring;
+ my $tag = Irssi::timeout_add_once $delay, \&disappeared,
+ [$chatnet, $nick, $address, $hdl];
+ $expire_auth{$chatnet}{$address}{$hdl} = $tag;
+ }
+}
+
+sub maybe_disappears($$$$$) {
+ my ($chatnet, $server, $channel, $nick, $address) = @_;
+ foreach my $chan ($server->channels()) {
+ next if defined $channel && lc $chan->{name} eq $channel;
+ return if $chan->nick_find_mask("*!$address");
+ }
+ disappears $chatnet, $nick, $address;
+}
+
+sub appears($$$) {
+ my ($chatnet, $nick, $address) = @_;
+ my $handles = $expire_auth{$chatnet}{$address} or return;
+ my @handles = keys %$handles;
+ foreach my $hdl (@handles) {
+ my $tag = $handles->{$hdl};
+ Irssi::timeout_remove $tag;
+ delete $handles->{$hdl};
+ }
+}
+
+our %queued_actions = ();
+
+our %action_not_needed = (
+ '+o' => sub {$_[0]->{op}},
+ '-o' => sub {not $_[0]->{op}},
+ '+v' => sub {$_[0]->{op} || $_[0]->{voice}},
+ '-v' => sub {$_[0]->{op} || not $_[0]->{voice}},
+);
+
+# Delete/create an appropriate timeout.
+sub queue_handle($$) {
+ my ($chatnet, $channel) = @_;
+ my $ref = $queued_actions{$chatnet}{$channel};
+ $ref->{queue} ||= [];
+
+ if (defined $ref->{tag} and @{ $ref->{queue} } == 0) {
+ Irssi::timeout_remove $ref->{tag};
+ delete $ref->{tag};
+ delete $ref->{time};
+ }
+
+ unless (@{ $ref->{queue} } == 0) {
+ my $time = $ref->{queue}[0]{time};
+ unless (defined $ref->{time} and $ref->{time} == $time) {
+ Irssi::timeout_remove $ref->{tag} if defined $ref->{tag};
+ $ref->{time} = $time;
+ my $delay = 1000 * ($time - Time::HiRes::time);
+ $delay = 10 if $delay < 10;
+ $ref->{tag} = Irssi::timeout_add_once $delay, \&queue_run,
+ [$chatnet, $channel];
+ }
+ }
+}
+
+# Run the first items from the queue.
+sub queue_run(\@) {
+ my ($chatnet, $channel) = @{ $_[0] };
+ delete $queued_actions{$chatnet}{$channel}{tag};
+ delete $queued_actions{$chatnet}{$channel}{time};
+
+ my $server = Irssi::server_find_chatnet $chatnet;
+ my $queue = $queued_actions{$chatnet}{$channel}{queue};
+ my $chan;
+ $chan = $server->channel_find($channel) if defined $server;
+ unless (defined $server and defined $chan) {
+ @$queue = ();
+ return;
+ }
+
+ my $max_modes = $server->isupport('modes') || 1;
+ my (@modes);
+ while (@modes < $max_modes and @$queue > 0) {
+ my $action = shift @$queue;
+ my $who = $chan->nick_find($action->{nick});
+ next unless defined $who;
+ next if $action_not_needed{$action->{action}}($who);
+ push @modes, [$action->{action}, $action->{nick}];
+ }
+
+ if (@modes) {
+ my ($mode_actions, @mode_params) = ('');
+ for my $mode (sort { $a->[0] cmp $b->[0] } @modes) {
+ $mode_actions .= $mode->[0];
+ push @mode_params, $mode->[1];
+ }
+ $server->command("mode $channel $mode_actions @mode_params");
+ }
+
+ queue_handle $chatnet, $channel;
+}
+
+sub queue_nick_changed($$$) {
+ my ($chatnet, $old_nick, $nick) = @_;
+ while (my ($channel, $ref) = each %{ $queued_actions{$chatnet} }) {
+ next unless defined $ref->{queue};
+ foreach (grep { $_->{nick} eq $old_nick } @{ $ref->{queue} }) {
+ $_->{nick} = $nick;
+ }
+ }
+}
+
+sub cancel_queued($$$) {
+ my ($chatnet, $channel, $nick) = @_;
+ my $queue = $queued_actions{$chatnet}{$channel}{queue};
+ return unless defined $queue;
+ @$queue = grep { $_->{nick} ne $nick } @$queue;
+ queue_handle $chatnet, $channel;
+}
+
+sub cancel_queued_everywhere($$) {
+ my ($chatnet, $nick) = @_;
+ while (my ($channel, $ref) = each %{ $queued_actions{$chatnet} }) {
+ cancel_queued $chatnet, $channel, $nick;
+ }
+}
+
+sub queue_action($$$$;$) {
+ my ($chatnet, $action, $channel, $nick, $delay) = @_;
+ unless (defined $delay) {
+ my $delay_min = Irssi::settings_get_int('people_op_delay_min');
+ my $delay_max = Irssi::settings_get_int('people_op_delay_max');
+ $delay_min = $delay_max if $delay_min > $delay_max;
+ $delay = $delay_min + rand ($delay_max - $delay_min);
+ }
+ my $queue = ($queued_actions{$chatnet}{$channel}{queue} ||= []);
+ @$queue = sort { $a->{time} <=> $b->{time} } @$queue, {
+ time => Time::HiRes::time + $delay,
+ action => $action,
+ nick => $nick
+ };
+ queue_handle $chatnet, $channel;
+}
+
+sub improve_mask($) {
+ my ($mask) = @_;
+ return "$1*" if $mask =~ /^(.*\@\d+\.\d+\.\d+\.)\d+$/;
+ return "$1*$2" if $mask =~ /^(.*\@)[^.]*\d[^.]*(\..*)$/;
+ return $mask;
+}
+
+sub ban($$$$$$) {
+ my ($server, $channel, $nick, $address, $is_op, $users) = @_;
+ my $mask = $users->{k} ? $users->{k}[1] : "*!" . improve_mask $address;
+ $server->command("mode $channel " . ($is_op ? "-o+b $nick $mask" : "+b $mask"));
+}
+
+sub kick($$$$) {
+ my ($server, $channel, $nick, $flags) = @_;
+ $server->command("kick $channel $nick" . ($flags->{k} eq '' ? "" : " $flags->{k}"));
+}
+
+sub execute($$$$$) {
+ my ($server, $channel, $nick, $address, $flags) = @_;
+ my $cmd = $flags->{e};
+ $cmd =~ s/\$([CNA])/{
+ C => $channel,
+ N => $nick,
+ A => $address,
+ }->{$1}/eg;
+ $server->command($cmd);
+}
+
+sub show_who($$$) {
+ my ($hdl, $nick, $address) = @_;
+ return
+ (defined $hdl ?
+ $hdl eq lc $nick ?
+ "\cc04$handles{$hdl}\co" :
+ $nick =~ s/\Q$hdl\E/\cc04$handles{$hdl}\cc11/i ?
+ "\cc11$nick\co" :
+ "\cc04$handles{$hdl}\co = \cc11$nick\co" :
+ "\cc11$nick\co") .
+ " \cc14[\cc10$address\cc14]\co";
+}
+
+sub notify($$$$$$) {
+ my ($nick, $address, $flags, $users, $str, $beep) = @_;
+ return unless defined $flags->{n};
+ my $hdl = $users->{''}[0];
+ $str =~ s/\{who\}/show_who $hdl, $nick, $address/eg;
+ print CLIENTCRAP $str . ($flags->{i} eq '' ? "" : " ($flags->{i})");
+ Irssi::command "beep" if $beep;
+}
+
+sub process_user($$$$$$$$) {
+ my ($server, $chan, $is_op, $is_voice, $nick, $address, $flags, $users) = @_;
+ return if defined $flags->{x};
+ return unless $chan->{chanop};
+ my $chatnet = lc $server->{chatnet};
+ my $channel = lc $chan->{name};
+ if (defined $flags->{r}) {
+ queue_action $chatnet, '+o', $channel, $nick unless $is_op;
+ } elsif (defined $flags->{o}) {
+ } elsif (defined $flags->{k}) {
+ ban $server, $channel, $nick, $address, $is_op, $users;
+ kick $server, $channel, $nick, $flags;
+ } elsif (defined $flags->{d}) {
+ queue_action $chatnet, '-o', $channel, $nick, 0.1 if $is_op;
+ }
+ if (defined $flags->{v}) {
+ } elsif (defined $flags->{q}) {
+ queue_action $chatnet, '-v', $channel, $nick, 0.2 if $is_voice;
+ }
+ if ($flags->{e} ne '') {
+ execute $server, $channel, $nick, $address, $flags;
+ }
+}
+
+Irssi::signal_add_last 'event join', sub {
+ my ($server, $args, $nick, $address) = @_;
+ $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return;
+ my $channel = lc $1;
+ return if $nick eq $server->{nick};
+ my $chatnet = lc $server->{chatnet};
+ my $chan = $server->channel_find($channel) or return;
+ appears $chatnet, $nick, $address;
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ notify $nick, $address, $flags, $users, "{who} has joined \cb$channel\cb", 1;
+ return if defined $flags->{x};
+ return unless $chan->{chanop};
+ if (defined $flags->{r} || defined $flags->{o}) {
+ queue_action $chatnet, '+o', $channel, $nick;
+ } elsif (defined $flags->{k}) {
+ ban $server, $channel, $nick, $address, 0, $users;
+ kick $server, $channel, $nick, $flags;
+ }
+ if (defined $flags->{v}) {
+ queue_action $chatnet, '+v', $channel, $nick;
+ }
+ if ($flags->{e} ne '') {
+ execute $server, $channel, $nick, $address, $flags;
+ }
+};
+
+sub process_channel($$$) {
+ my ($server, $chan, $notify) = @_;
+ my $chatnet = lc $server->{chatnet};
+ my $channel = lc $chan->{name};
+ foreach my $who ($chan->nicks()) {
+ my $nick = $who->{nick};
+ next if $nick eq $server->{nick};
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ notify $nick, $address, $flags, $users,
+ "{who} is on \cb$channel\cb", 0 if $notify;
+ process_user $server, $chan, $who->{op}, $who->{voice}, $nick, $address, $flags, $users;
+ }
+}
+
+Irssi::signal_add_last 'channel wholist', sub {
+ my ($chan) = @_;
+ my $server = $chan->{server};
+ my $chatnet = lc $server->{chatnet};
+ foreach my $who ($chan->nicks()) {
+ appears $chatnet, $who->{nick}, $who->{host};
+ }
+ process_channel $server, $chan, 1;
+};
+
+Irssi::signal_add_first 'channel destroyed', sub {
+ my ($chan) = @_;
+ my $server = $chan->{server};
+ my $chatnet = lc $server->{chatnet};
+ foreach my $who ($chan->nicks()) {
+ maybe_disappears $chatnet, $server, lc $chan->{name}, $who->{nick}, $who->{host};
+ }
+};
+
+sub is_master($$$$) {
+ my ($chatnet, $chan, $channel, $nick) = @_;
+ return 1 if $nick eq $chan->{server}{nick};
+ my $who = $chan->nick_find($nick);
+ my $address = $who ? $who->{host} : '';
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ return defined $flags->{m};
+}
+
+Irssi::signal_add_last 'nick mode changed', sub {
+ my ($chan, $who, $setter) = @_;
+ my $server = $chan->{server};
+ my $nick = $who->{nick};
+ if ($nick eq $server->{nick}) {
+ return unless $chan->{chanop};
+ process_channel $server, $chan, 0 if $chan->{wholist};
+ } else {
+ my $chatnet = lc $server->{chatnet};
+ my $channel = lc $chan->{name};
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ return if defined $flags->{x};
+ return unless $chan->{chanop};
+ if (defined $flags->{r}) {
+ queue_action $chatnet, '+o', $channel, $nick
+ unless $who->{op} ||
+ $setter eq $nick ||
+ is_master($chatnet, $chan, $channel, $setter);
+ } elsif (defined $flags->{o}) {
+ } elsif (defined $flags->{d}) {
+ queue_action $chatnet, '-o', $channel, $nick, 0.1
+ unless !$who->{op} ||
+ is_master($chatnet, $chan, $channel, $setter);
+ }
+ if (defined $flags->{v}) {
+ } elsif (defined $flags->{q}) {
+ queue_action $chatnet, '-v', $channel, $nick, 0.2
+ unless !$who->{voice} ||
+ is_master($chatnet, $chan, $channel, $setter);
+ }
+ }
+};
+
+Irssi::signal_add_last 'event part', sub {
+ my ($server, $args, $nick, $address) = @_;
+ $args =~ /^([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+)$/ or $args =~ /^([^ ]+)()$/ or return;
+ my ($channel, $reason) = (lc $1, $2);
+ my $chatnet = lc $server->{chatnet};
+ my $chan = $server->channel_find($channel) or return;
+ maybe_disappears $chatnet, $server, $channel, $nick, $address;
+ cancel_queued $chatnet, $channel, $nick;
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ notify $nick, $address, $flags, $users,
+ "{who} has left \cb$channel\cb \cc14[\co$reason\cc14]\co", 0;
+};
+
+Irssi::signal_add_last 'event quit', sub {
+ my ($server, $args, $nick, $address) = @_;
+ $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or $args =~ /^()$/ or return;
+ my $reason = $1;
+ my $chatnet = lc $server->{chatnet};
+ maybe_disappears $chatnet, $server, undef, $nick, $address;
+ cancel_queued_everywhere $chatnet, $nick;
+ my ($flags, $users) = find_global_flags $chatnet, $nick, $address;
+ delete $flags->{n};
+ foreach my $chan ($server->channels()) {
+ next unless $chan->nick_find($nick);
+ my $channel = lc $chan->{name};
+ my ($local_flags, $local_users) = find_local_flags $chatnet, $channel, $nick, $address;
+ if (defined $local_flags->{n}) {
+ $flags->{n} = '';
+ last;
+ }
+ }
+ notify $nick, $address, $flags, $users,
+ "{who} has quit \cc14[\co$reason\cc14]\co", 0;
+};
+
+Irssi::signal_add_last 'event kick', sub {
+ my ($server, $args, $kicker, $kicker_address) = @_;
+ $args =~ /^([^ ]+) +([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+) +([^ ]+)$/ or
+ $args =~ /^([^ ]+) +([^ ]+)()$/ or return;
+ my ($channel, $nick, $reason) = (lc $1, $2, $3);
+ my $chatnet = lc $server->{chatnet};
+ my $chan = $server->channel_find($channel) or return;
+ my $who = $chan->nick_find($nick);
+ return unless defined $who;
+ my $address = $who->{host};
+ maybe_disappears $chatnet, $server, $channel, $nick, $address;
+ cancel_queued $chatnet, $channel, $nick;
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ notify $nick, $address, $flags, $users,
+ "{who} was kicked from \cb$channel\cb by \cb$kicker\cb \cc14[\co$reason\cc14]\co", 0;
+};
+
+Irssi::signal_add_last 'event nick', sub {
+ my ($server, $args, $old_nick, $address) = @_;
+ $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return;
+ my $new_nick = $1;
+ my $chatnet = lc $server->{chatnet};
+ queue_nick_changed $chatnet, $old_nick, $new_nick;
+ foreach my $chan ($server->channels()) {
+ my @nicks = map {$_->{nick}} $chan->nicks();
+ my $who = $chan->nick_find($new_nick);
+ next unless $who;
+ my $channel = lc $chan->{name};
+ my ($old_flags, $old_users) = find_local_flags $chatnet, $channel, $old_nick, $address;
+ my ($new_flags, $new_users) = find_local_flags $chatnet, $channel, $new_nick, $address;
+ if (defined $new_flags->{n} &&
+ (!defined $old_flags->{n} || $old_users->{''}[0] ne $new_users->{''}[0])) {
+ notify $new_nick, $address, $new_flags, $new_users,
+ "{who} is on \cb$channel\cb", 1;
+ }
+ next if defined $new_flags->{x};
+ next unless $chan->{chanop};
+ if (defined $new_flags->{o}) {
+ queue_action $chatnet, '+o', $channel, $new_nick
+ if !defined $old_flags->{o} && !$who->{op};
+ } elsif (defined $new_flags->{k}) {
+ ban $server, $channel, $new_nick, $address, $who->{op}, $new_users;
+ kick $server, $channel, $new_nick, $new_flags;
+ } elsif (defined $new_flags->{d}) {
+ queue_action $chatnet, '-o', $channel, $new_nick, 0.1
+ if !defined $old_flags->{d} && $who->{op};
+ }
+ if (defined $new_flags->{v}) {
+ queue_action $chatnet, '+v', $channel, $new_nick
+ if !defined $old_flags->{v} && !$who->{op} && !$who->{voice};
+ } elsif (defined $new_flags->{q}) {
+ queue_action $chatnet, '-v', $channel, $new_nick, 0.2
+ if !defined $old_flags->{q} && $who->{voice};
+ }
+ if ($new_flags->{e} ne '') {
+ execute $server, $channel, $new_nick, $address, $new_flags;
+ }
+ }
+};
+
+######## NICK COLORS ########
+
+sub compute_color($) {
+ my ($text) = @_;
+ my $sum = 0;
+ foreach my $ch (lc($text) =~ /[a-z]/g) {
+ $sum += ord $ch;
+ }
+ my @colors = split(//, Irssi::settings_get_str('people_colors'));
+ return '%' . $colors[$sum % @colors];
+}
+
+Irssi::signal_add_last 'message public', sub {
+ my ($server, $msg, $nick, $address, $channel) = @_;
+ my $chatnet = lc $server->{chatnet};
+ $channel = lc $channel;
+ my $chan = $server->channel_find($channel) or return;
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ return unless defined $flags->{c} ||
+ Irssi::settings_get_bool('people_color_friends') && defined $flags->{''} ||
+ Irssi::settings_get_bool('people_color_everybody');
+ my $color = $flags->{c} ne '' ? $flags->{c} :
+ compute_color(defined $flags->{c} && $users->{c} ? $handles{$users->{c}[0]} :
+ defined $flags->{''} ? $handles{$users->{''}[0]} : $nick);
+ my $window = $server->window_find_item($channel);
+ my $theme = $window->{theme} || Irssi::current_theme;
+ my $oform = $theme->get_format('fe-common/core', 'pubmsg');
+ my $nform = $oform;
+ $nform =~ s/(\$(?:\[-?\d+\])?0)/$color$1%n/g;
+ $window->command("^format pubmsg $nform") if $window;
+ Irssi::signal_continue @_;
+ $window->command("^format pubmsg $oform") if $window;
+};
+
+######## WORK WHEN USERLIST CHANGED ########
+
+sub user_changed_on_channel($$$$$) {
+ my ($hdl, $server, $chatnet, $chan, $channel) = @_;
+ foreach my $who ($chan->nicks()) {
+ my $nick = $who->{nick};
+ next if $nick eq $server->{nick};
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags_if_matches $hdl, $chatnet, $channel, $nick, $address;
+ notify $nick, $address, $flags, $users,
+ "{who} is on \cb$channel\cb", 0;
+ process_user $server, $chan, $who->{op}, $who->{voice}, $nick, $address, $flags, $users;
+ }
+}
+
+sub user_changed($) {
+ my ($hdl) = @_;
+ foreach my $server (Irssi::servers) {
+ my $chatnet = lc $server->{chatnet};
+ foreach my $chan ($server->channels()) {
+ next unless $chan->{wholist};
+ my $channel = lc $chan->{name};
+ user_changed_on_channel $hdl, $server, $chatnet, $chan, $channel;
+ }
+ }
+}
+
+sub user_channel_changed($$$) {
+ my ($hdl, $chatnet, $channel) = @_;
+ my $server = Irssi::server_find_chatnet $chatnet or return;
+ my $chan = $server->channel_find($channel) or return;
+ user_changed_on_channel $hdl, $server, $chatnet, $chan, $channel;
+}
+
+sub channel_changed($$) {
+ my ($chatnet, $channel) = @_;
+ my $server = Irssi::server_find_chatnet $chatnet or return;
+ my $chan = $server->channel_find($channel) or return;
+ process_channel $server, $chan, 0 if $chan->{wholist};
+}
+
+sub all_changed() {
+ foreach my $server (Irssi::servers) {
+ foreach my $chan ($server->channels()) {
+ process_channel $server, $chan, 0 if $chan->{wholist};
+ }
+ }
+}
+
+######## STORE CONFIGURATION IN A FILE ########
+
+sub show_flag($$) {
+ my ($flag, $arg) = @_;
+ return defined $arg ? $arg eq '' ? "+$flag" : "+$flag $arg" : "-$flag";
+}
+
+sub save_config() {
+ open CONFIG, ">$config_tmp";
+ foreach my $hdl (sort keys %handles) {
+ my $handle = $handles{$hdl};
+ my @masks = sort @{$user_masks{$hdl}};
+ print CONFIG "user $handle @masks\n";
+ my $globals = $user_flags{$hdl};
+ foreach my $flag (sort keys %$globals) {
+ print CONFIG "flag $handle " .
+ show_flag($flag, $globals->{$flag}) . "\n";
+ }
+ my $chatnets = $user_channel_flags{$hdl};
+ foreach my $chatnet (sort keys %$chatnets) {
+ my $channels = $chatnets->{$chatnet};
+ foreach my $channel (sort keys %$channels) {
+ my $locals = $channels->{$channel};
+ foreach my $flag (sort keys %$locals) {
+ print CONFIG "flag $handle $chatnet/$channel " .
+ show_flag($flag, $locals->{$flag}) . "\n";
+ }
+ }
+ }
+ print CONFIG "\n";
+ }
+ foreach my $chatnet (sort keys %channel_flags) {
+ my $channels = $channel_flags{$chatnet};
+ foreach my $channel (sort keys %$channels) {
+ my $flags = $channels->{$channel};
+ next unless %$flags;
+ foreach my $flag (sort keys %$flags) {
+ print CONFIG "flag $chatnet/$channel " .
+ show_flag($flag, $flags->{$flag}) . "\n";
+ }
+ print CONFIG "\n";
+ }
+ }
+ close CONFIG;
+ rename $config, $config_old;
+ rename $config_tmp, $config;
+}
+
+sub autosave_config() {
+ save_config if Irssi::settings_get_bool 'people_autosave';
+}
+
+Irssi::signal_add 'setup saved', sub {
+ my ($main_config, $auto) = @_;
+ save_config unless $auto;
+};
+
+sub unique_masks(@) {
+ my %masks = ();
+ foreach my $mask (@_) {
+ $mask = "*\@$mask" if $mask !~ /\@|!\*$/;
+ $mask = "*!$mask" if $mask !~ /!/;
+ $masks{$mask} = 1;
+ }
+ return sort keys %masks;
+}
+
+sub load_config() {
+ %handles = ();
+ %user_masks = ();
+ %user_flags = ();
+ %channel_flags = ();
+ %user_channel_flags = ();
+ open CONFIG, $config or return;
+ while (<CONFIG>) {
+ chomp;
+ next if /^ *$/ || /^#/;
+ if (/^user +$handle_re$opt_masks_re *$/o) {
+ my ($handle, $masks) = ($1, $2);
+ $handles{lc $handle} = $handle;
+ $user_masks{lc $handle} = [unique_masks(split(' ', $masks))];
+ } elsif (/^flag +$handle_re +$chatnet_re\/$channel_re +\+([a-zA-Z])$arg_re$/o) {
+ my ($handle, $chatnet, $channel, $flag, $arg) = ($1, $2, $3, $4, $5);
+ $flag = tr_flag $flag;
+ $arg = '' unless defined $arg;
+ $user_channel_flags{lc $handle}{$chatnet}{$channel}{$flag} = $arg;
+ } elsif (/^flag +$handle_re +$chatnet_re\/$channel_re +-([a-zA-Z]) *$/o) {
+ my ($handle, $chatnet, $channel, $flag) = ($1, $2, $3, $4);
+ $flag = tr_flag $flag;
+ $user_channel_flags{lc $handle}{$chatnet}{$channel}{$flag} = undef;
+ } elsif (/^flag +$chatnet_re\/$channel_re +\+([a-zA-Z])$arg_re$/o) {
+ my ($chatnet, $channel, $flag, $arg) = ($1, $2, $3, $4);
+ $flag = tr_flag $flag;
+ $arg = '' unless defined $arg;
+ $channel_flags{$chatnet}{$channel}{$flag} = $arg;
+ } elsif (/^flag +$chatnet_re\/$channel_re +-([a-zA-Z]) *$/o) {
+ my ($chatnet, $channel, $flag) = ($1, $2, $3);
+ $flag = tr_flag $flag;
+ $channel_flags{$chatnet}{$channel}{$flag} = undef;
+ } elsif (/^flag +$handle_re +\+([a-zA-Z])$arg_re$/o) {
+ my ($handle, $flag, $arg) = ($1, $2, $3);
+ $flag = tr_flag $flag;
+ $arg = '' unless defined $arg;
+ $user_flags{lc $handle}{$flag} = $arg;
+ } elsif (/^flag +$handle_re +-([a-zA-Z]) *$/o) {
+ my ($handle, $flag) = ($1, $2);
+ $flag = tr_flag $flag;
+ $user_flags{lc $handle}{$flag} = undef;
+ } else {
+ print CLIENTERROR "Syntax error in $config: $_";
+ }
+ }
+ update_all_masks;
+ all_changed;
+}
+
+Irssi::signal_add 'setup reread', \&load_config;
+
+######## MANAGE THE USER LIST ########
+
+sub find_nick($) {
+ my ($nick) = @_;
+ foreach my $chan (Irssi::channels) {
+ my $who = $chan->nick_find($nick) or next;
+ my $address = $who->{host};
+ return $address if $address ne '';
+ }
+ return undef;
+}
+
+sub find_server_nick($$) {
+ my ($server, $nick) = @_;
+ foreach my $chan ($server->channels) {
+ my $who = $chan->nick_find($nick) or next;
+ my $address = $who->{host};
+ return $address if $address ne '';
+ }
+ return undef;
+}
+
+sub guess_mask($) {
+ my ($nick) = @_;
+ my $address = find_nick $nick;
+ return defined $address ? (improve_mask $address) : ();
+}
+
+sub cmd_user_add($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ unless ($args =~ /^ *$handle_re$opt_masks_re *$/o) {
+ $context->{usage}("user add <handle> <mask>...");
+ return;
+ }
+ my ($handle, $masks) = ($1, $2);
+ my $hdl = lc $handle;
+ if (defined $handles{$hdl}) {
+ $context->{error}("User \cc04$handles{$hdl}\co already exists");
+ return;
+ }
+ my @masks = split(' ', $masks);
+ @masks = guess_mask $handle unless @masks;
+ @masks = unique_masks(@masks);
+ $handles{$hdl} = $handle;
+ $user_masks{$hdl} = [@masks];
+ $user_flags{$hdl}{l} = ''
+ unless $context->{owner} || defined $context->{globals}{m};
+ if (@masks) {
+ my $plural = @masks == 1 ? "" : "s";
+ $context->{notice}("Added user \cc04$handle\co with address mask$plural \cc10@masks\co");
+ } else {
+ $context->{notice}("Added user \cc04$handle\co with no address masks.");
+ }
+ update_all_masks;
+ user_changed $hdl;
+ autosave_config;
+}
+
+sub cmd_user_remove($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ unless ($args =~ /^ *$handle_re *$/o) {
+ $context->{usage}("user remove <handle>");
+ return;
+ }
+ my $handle = $1;
+ handle_exists $context, $handle or return;
+ my $hdl = lc $handle;
+ may_manage $context, $hdl or return;
+ $context->{notice}("Removed user \cc04$handles{$hdl}\co.");
+ delete $user_flags{$hdl};
+ delete $user_channel_flags{$hdl};
+ user_changed $hdl;
+ delete $handles{$hdl};
+ delete $user_masks{$hdl};
+ update_all_masks;
+ autosave_config;
+};
+
+sub cmd_mask_add($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ unless ($args =~ /^ *$handle_re +$masks_re *$/o) {
+ $context->{usage}("mask add <handle> <mask>...");
+ return;
+ }
+ my ($handle, $masks) = ($1, $2);
+ handle_exists $context, $handle or return;
+ my $hdl = lc $handle;
+ may_manage $context, $hdl or return;
+ my %masks = map {$_ => 1} @{$user_masks{$hdl}};
+ foreach my $mask (unique_masks(split(' ', $masks))) {
+ $masks{$mask} = 1;
+ }
+ $user_masks{$hdl} = [sort keys %masks];
+ show_handle $context, $hdl;
+ update_all_masks;
+ user_changed $hdl;
+ autosave_config;
+}
+
+sub cmd_mask_remove($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ unless ($args =~ /^ *$handle_re +$masks_re *$/o) {
+ $context->{usage}("mask remove <handle> <mask>...");
+ return;
+ }
+ my ($handle, $masks) = ($1, $2);
+ handle_exists $context, $handle or return;
+ my $hdl = lc $handle;
+ may_manage $context, $hdl or return;
+ my %masks = map {$_ => 1} @{$user_masks{$hdl}};
+ foreach my $mask (unique_masks(split(' ', $masks))) {
+ delete $masks{$mask};
+ }
+ $user_masks{$hdl} = [sort keys %masks];
+ show_handle $context, $hdl;
+ update_all_masks;
+ user_changed $hdl;
+ autosave_config;
+}
+
+sub cmd_user_rename($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ unless ($args =~ /^ *$handle_re +$handle_re *$/o) {
+ $context->{usage}("user rename <handle> <new-handle>");
+ return;
+ }
+ my ($old_handle, $new_handle) = ($1, $2);
+ handle_exists $context, $old_handle or return;
+ my $old_hdl = lc $old_handle;
+ my $new_hdl = lc $new_handle;
+ may_manage $context, $old_hdl or return;
+ if ($new_hdl ne $old_hdl && defined $handles{$new_hdl}) {
+ $context->{error}("User \cc04$handles{$new_hdl}\co already exists.");
+ return;
+ }
+ $handles{$new_hdl} = $new_handle;
+ if ($new_hdl ne $old_hdl) {
+ delete $handles{$old_hdl};
+ $user_masks{$new_hdl} = $user_masks{$old_hdl};
+ delete $user_masks{$old_hdl};
+ if ($user_flags{$old_hdl}) {
+ $user_flags{$new_hdl} = $user_flags{$old_hdl};
+ delete $user_flags{$old_hdl};
+ }
+ if ($user_channel_flags{$old_hdl}) {
+ $user_channel_flags{$new_hdl} = $user_channel_flags{$old_hdl};
+ delete $user_channel_flags{$old_hdl};
+ }
+ }
+ $context->{notice}("Renamed user \cc04$old_handle\co to \cc04$new_handle\co.");
+ autosave_config;
+}
+
+######## MANAGE FLAGS ########
+
+sub flag_usage($) {
+ my ($context) = @_;
+ $context->{usage} ("flag <handle>");
+ $context->{usage_next}("flag [<chatnet>/]<#channels>");
+ $context->{usage_next}("flag <handle> <flags>");
+ $context->{usage_next}("flag [<chatnet>/]<#channels> <flags>");
+ $context->{usage_next}("flag <handle> [<chatnet>/]<#channels> <flags>");
+ $context->{error}("<flags> is (+<letter>...|-<letter>...)...");
+ $context->{error}("The last +<letter> may be followed by space and <argument>");
+}
+
+sub parse_flags($) {
+ my ($flags) = @_;
+ return map {
+ my ($dir, $force) = /^\+/ ? ('', 0) : /^-/ ? (undef, 0) : (undef, 1);
+ map {[$_, $dir, $force]} (/[a-zA-Z]/g)
+ } ($flags =~ /[+\-!][a-zA-Z]+/g);
+}
+
+sub cmd_flag($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ if ($args =~ /^ *(?:$chatnet_re\/)?$channels_re *$/o) {
+ my ($chatnet, $channels) = ($1, lc $2);
+ $chatnet = default_chatnet $context unless defined $chatnet;
+ $chatnet = lc $chatnet;
+ foreach my $channel (split /,/, $channels) {
+ show_channel $context, $chatnet, $channel, 1;
+ }
+ return;
+ }
+ if ($args =~ /^ *$handle_re *$/o) {
+ my ($hdl) = lc $1;
+ show_handle $context, $hdl;
+ return;
+ }
+ unless ($args =~ /^ *(?:$handle_re +)??(?:(?:$chatnet_re\/)?$channels_re +)?$flags_re$arg_re$/o) {
+ flag_usage $context; return;
+ }
+ my ($handle, $chatnet, $channels, $flags, $arg) = ($1, $2, $3, $4, $5);
+ unless (defined $handle || defined $channels) {
+ flag_usage $context; return;
+ }
+ $arg = '' unless defined $arg;
+ if (defined $handle) {
+ handle_exists $context, $handle or return;
+ }
+ my $hdl = lc $handle;
+ my @channels = ();
+ if (defined $channels) {
+ $chatnet = default_chatnet $context unless defined $chatnet;
+ $chatnet = lc $chatnet;
+ @channels = map {[$chatnet, lc $_]} split /,/, $channels;
+ }
+ my @changes = parse_flags $flags;
+ if ($arg ne '') {
+ unless (defined $changes[$#changes][1]) {
+ flag_usage $context; return;
+ }
+ $changes[$#changes][1] = $arg;
+ }
+ foreach my $change (@changes) {
+ my ($flag, $arg, $force) = @$change;
+ my $new_flag = tr_flag $flag;
+ if ($new_flag ne $flag) {
+ $context->{error}("Please use \cc9+$new_flag\co instead of \cc9+$flag\co.");
+ $flag = $new_flag;
+ $change->[0] = $flag;
+ }
+ unless ($context->{set_flags}{$flag}) {
+ if ($context->{owner}) {
+ $context->{error}("Warning, only flags \cc9$context->{set_flags_str}\co are meaningful.");
+ } else {
+ $context->{error}("Sorry, you can only set flags \cc9$context->{set_flags_str}\co.");
+ return;
+ }
+ }
+ }
+ unless ($context->{owner} || defined $context->{globals}{m}) {
+ if (@channels) {
+ foreach my $chatnet_channel (@channels) {
+ my ($chatnet, $channel) = @$chatnet_channel;
+ unless (defined $context->{locals}{$chatnet}{$channel}{m}) {
+ $context->{error}("Sorry, you don't have master privileges in \cb$channel\cb.");
+ return;
+ }
+ }
+ } else {
+ my $chatnets = $context->{locals};
+ foreach my $chatnet (keys %$chatnets) {
+ my $channels = $chatnets->{$chatnet};
+ foreach my $channel (keys %$channels) {
+ my $flags = $channels->{$channel};
+ push @channels, [$chatnet, $channel] if defined $flags->{m};
+ }
+ }
+ }
+ }
+ if (defined $handle) {
+ if (@channels) {
+ foreach my $chatnet_channel (@channels) {
+ my ($chatnet, $channel) = @$chatnet_channel;
+ my $flags = \%{$user_channel_flags{$hdl}{$chatnet}{$channel}};
+ foreach my $change (@changes) {
+ my ($flag, $arg, $force) = @$change;
+ my $global =
+ exists $channel_flags{$chatnet}{$channel}{$flag} ?
+ $channel_flags{$chatnet}{$channel}{$flag} :
+ $user_flags{$hdl}{$flag};
+ if ($force ||
+ defined $arg != defined $global ||
+ defined $arg && defined $global &&
+ $arg ne $global && $arg ne '') {
+ $flags->{$flag} = $arg;
+ } else {
+ delete $flags->{$flag};
+ }
+ }
+ }
+ show_handle $context, $hdl;
+ foreach my $chatnet_channel (@channels) {
+ my ($chatnet, $channel) = @$chatnet_channel;
+ user_channel_changed $hdl, $chatnet, $channel;
+ }
+ } else {
+ my $flags = \%{$user_flags{$hdl}};
+ foreach my $change (@changes) {
+ my ($flag, $arg, $force) = @$change;
+ if ($force || defined $arg) {
+ $flags->{$flag} = $arg;
+ } else {
+ delete $flags->{$flag};
+ }
+ }
+ show_handle $context, $hdl;
+ user_changed $hdl;
+ }
+ } else {
+ foreach my $chatnet_channel (@channels) {
+ my ($chatnet, $channel) = @$chatnet_channel;
+ my $flags = \%{$channel_flags{$chatnet}{$channel}};
+ foreach my $change (@changes) {
+ my ($flag, $arg, $force) = @$change;
+ if ($force || defined $arg) {
+ $flags->{$flag} = $arg;
+ } else {
+ delete $flags->{$flag};
+ }
+ }
+ show_channel $context, $chatnet, $channel, 1;
+ channel_changed $chatnet, $channel;
+ }
+ }
+ autosave_config;
+}
+
+######## FIND USERS ########
+
+sub cmd_find($$) {
+ my ($context, $args) = @_;
+ if ($args =~ /^ *(?:$chatnet_re\/)?$channel_re *$/o) {
+ my ($chatnet, $channel) = ($1, lc $2);
+ must_be_master $context or return;
+ $chatnet = default_chatnet $context unless defined $chatnet;
+ $chatnet = lc $chatnet;
+ my $server = Irssi::server_find_chatnet $chatnet;
+ unless ($server) {
+ $context->{error}("Sorry, I'm not connected to $chatnet.");
+ return;
+ }
+ my $chan = $server->channel_find($channel);
+ unless ($chan) {
+ $context->{error}("Sorry, I'm not on $channel.");
+ }
+ my @people = ();
+ foreach my $who ($chan->nicks()) {
+ my $nick = $who->{nick};
+ next if $nick eq $server->{nick};
+ my $address = $who->{host};
+ my ($hdl, $mask) = find_best_user undef, $nick, $address;
+ next unless defined $hdl;
+ push @people, [$hdl, $nick, $address];
+ }
+ unless (@people) {
+ $context->{crap}("I don't recognize any people from \cb$channel\cb.");
+ return;
+ }
+ $context->{crap}("Recognized people on \cb$channel\cb:");
+ foreach my $person (sort {$a->[0] cmp $b->[0]} @people) {
+ my ($hdl, $nick, $address) = @$person;
+ $context->{crap}(show_who $hdl, $nick, $address);
+ }
+ } elsif ($args =~ /^ *$mask_re *$/o) {
+ my $mask = $1;
+ must_be_master $context or return;
+ my ($nick, $address);
+ if ($mask =~ /^(.*)!(.*)$/) {
+ ($nick, $address) = ($1, $2);
+ } elsif ($mask =~ /\@/) {
+ ($nick, $address) = ('*', $mask);
+ } else {
+ $nick = $mask;
+ $address = find_nick $nick;
+ unless (defined $address) {
+ $context->{error}("I don't see \cc11$nick\co on my channels.");
+ return;
+ }
+ }
+ my @users = find_users undef, $nick, $address;
+ unless (@users) {
+ $context->{error}("I don't know who \cc11$nick\co \cc14[\cc10$address\cc14]\co is.");
+ return;
+ }
+ foreach my $user (@users) {
+ my ($hdl, $mask) = @$user;
+ my $who = show_who $hdl, $nick, $address;
+ $context->{crap}("$who \cc14(\cc10$mask\cc14)\co");
+ }
+ } elsif ($context->{owner} && $args =~ /^ *$/) {
+ my %people = ();
+ my %channels = ();
+ foreach my $server (Irssi::servers) {
+ my $chatnet = lc $server->{chatnet};
+ foreach my $chan ($server->channels()) {
+ my $channel = lc $chan->{name};
+ foreach my $who ($chan->nicks()) {
+ my $nick = $who->{nick};
+ next if $nick eq $server->{nick};
+ my $address = $who->{host};
+ my ($hdl, $mask) = find_best_user undef, $nick, $address;
+ next unless defined $hdl;
+ $people{$chatnet}{$nick} = [$address, $hdl];
+ push @{$channels{$chatnet}{$nick}}, $channel;
+ }
+ }
+ }
+ my @people = ();
+ foreach my $chatnet (keys %people) {
+ my $nicks = $people{$chatnet};
+ foreach my $nick (keys %$nicks) {
+ my ($address, $hdl) = @{$nicks->{$nick}};
+ my $channels = $channels{$chatnet}{$nick};
+ push @people, [$hdl, $chatnet, $nick, $address, $channels];
+ }
+ }
+ foreach my $person (sort {$a->[0] cmp $b->[0]} @people) {
+ my ($hdl, $chatnet, $nick, $address, $channels) = @$person;
+ my $who = show_who $hdl, $nick, $address;
+ my $channels_txt = join(", ", sort @$channels);
+ $context->{crap}("\cc14[\co$chatnet\cc14]\co $who is on \cb$channels_txt\cb");
+ }
+ } else {
+ if ($context->{owner}) {
+ $context->{usage} ("find");
+ $context->{usage_next}("find <#channel>");
+ } else {
+ $context->{usage} ("find <#channel>");
+ }
+ $context->{usage_next}("find <mask>");
+ $context->{usage_next}("find <nick>");
+ }
+};
+
+######## OPERATOR COMMANDS ########
+
+sub find_channel($$$) {
+ my ($context, $channel, $need_op) = @_;
+ my $chan = $context->{server}->channel_find($channel);
+ if ($chan) {
+ if ($need_op && !$chan->{chanop}) {
+ $context->{error}("Sorry, I'm not an operator on \cb$channel\cb.");
+ return undef;
+ }
+ return $chan;
+ } else {
+ $context->{error}("Sorry, I'm not on \cb$channel\cb.");
+ return undef;
+ }
+}
+
+sub must_be_channel_operator($$$) {
+ my ($context, $chatnet, $channel) = @_;
+ return 1 if has_local_flag($context, $chatnet, $channel, 'o') ||
+ has_local_flag($context, $chatnet, $channel, 'm');
+ $context->{error}("Sorry, you don't have operator privileges on \cb$channel\cb.");
+ return 0;
+}
+
+sub cmd_trust($$) {
+ my ($context, $args) = @_;
+ must_be_master $context or return;
+ my @nicks = map { lc } split /\s+/, $args;
+ my $chatnet = lc default_chatnet $context;
+ my $server = Irssi::server_find_chatnet $chatnet;
+ foreach my $nick (@nicks) {
+ my $address = find_server_nick $server, $nick;
+ unless (defined $address) {
+ $context->{error}("I don't see \cc11$nick\co in \cb$chatnet\cb.");
+ next;
+ }
+ my @users = find_users undef, $nick, $address;
+ unless (@users) {
+ $context->{error}("I don't recognize \cc11$nick\co.");
+ }
+ foreach my $user (@users) {
+ my ($hdl, $mask) = @$user;
+ unless (defined $user_flags{$hdl}{p}) {
+ $context->{error}("\cc04$hdl\co doesn't need a password.");
+ next;
+ }
+ $context->{notice}("Trusting \cc11$nick\co to be \cc04$hdl\co " .
+ "on \cb$chatnet\cb.");
+ $authenticated{$chatnet}{$address}{$hdl} = 1;
+ maybe_disappears $chatnet, $server, undef, $nick, $address;
+ foreach my $chan ($server->channels()) {
+ next unless $chan->{wholist};
+ next unless $chan->{chanop};
+ my $channel = lc $chan->{name};
+ # nick_find_mask() only returns one nick.
+ foreach my $who (grep { $_->{host} eq $address } $chan->nicks()) {
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ next if defined $flags->{x};
+ if (defined $flags->{r} || defined $flags->{o}) {
+ queue_action $chatnet, '+o', $channel, $who->{nick};
+ }
+ if (defined $flags->{v}) {
+ queue_action $chatnet, '+v', $channel, $who->{nick};
+ }
+ # FIXME: flag +e?
+ }
+ }
+ }
+ }
+}
+
+sub cmd_op($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re(?: +$nicks_re)? *$/o) {
+ $context->{usage}("op <#channel> [<nick>]...");
+ return;
+ }
+ my ($channel, $nicks) = (lc $1, $2);
+ my @nicks = defined $nicks ? split ' ', $nicks : ($context->{nick});
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @good = ();
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ next if $who->{op};
+ unless (has_local_flag($context, $chatnet, $channel, 'm')) {
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ if (!defined $flags->{o} && defined $flags->{d}) {
+ $context->{error}("I refuse to op \cb$nick\cb on \cb$channel\cb - has \cc9+d\co flag.");
+ next;
+ }
+ }
+ push @good, $nick;
+ }
+ if (@good) {
+ my $cmd = "+" . "o" x @good . " @good";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ }
+}
+
+sub cmd_deop($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re(?: +$nicks_re)? *$/o) {
+ $context->{usage}("deop <#channel> [<nick>]...");
+ return;
+ }
+ my ($channel, $nicks) = (lc $1, $2);
+ my @nicks = defined $nicks ? split ' ', $nicks : ($context->{nick});
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @good = ();
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ next unless $who->{op};
+ unless (has_local_flag($context, $chatnet, $channel, 'm')) {
+ if ($nick eq $server->{nick}) {
+ $context->{error}("I refuse to deop myself on \cb$channel\cb.");
+ next;
+ }
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ if (defined $flags->{r} && $nick ne $context->{nick}) {
+ $context->{error}("I refuse to deop \cb$nick\cb on \cb$channel\cb - has \cc9+r\co flag.");
+ next;
+ }
+ }
+ push @good, $nick;
+ }
+ if (@good) {
+ my $cmd = "-" . "o" x @good . " @good";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ }
+}
+
+sub cmd_voice($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re(?: +$nicks_re)? *$/o) {
+ $context->{usage}("voice <#channel> [<nick>]...");
+ return;
+ }
+ my ($channel, $nicks) = (lc $1, $2);
+ my @nicks = defined $nicks ? split ' ', $nicks : ($context->{nick});
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @good = ();
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ next if $who->{voice};
+ unless (has_local_flag($context, $chatnet, $channel, 'm')) {
+ my $address = $who->{host};
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ if (!defined $flags->{v} && defined $flags->{q}) {
+ $context->{error}("I refuse to voice \cb$nick\cb on \cb$channel\cb - has \cc9+q\co flag.");
+ next;
+ }
+ }
+ push @good, $nick;
+ }
+ if (@good) {
+ my $cmd = "+" . "v" x @good . " @good";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ }
+}
+
+sub cmd_devoice($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re(?: +$nicks_re)? *$/o) {
+ $context->{usage}("devoice <#channel> [<nick>]...");
+ return;
+ }
+ my ($channel, $nicks) = (lc $1, $2);
+ my @nicks = defined $nicks ? split ' ', $nicks : ($context->{nick});
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @good = ();
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ next unless $who->{voice};
+ push @good, $nick;
+ }
+ if (@good) {
+ my $cmd = "-" . "v" x @good . " @good";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ }
+}
+
+sub cmd_kick($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re +$nicks_commas_re(| .*)$/o) {
+ $context->{usage}("kick <#channel> <nicks> [<reason>]");
+ return;
+ }
+ my ($channel, $nicks, $reason) = (lc $1, $2, $3);
+ my @nicks = split /,/, $nicks;
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ $reason = " $context->{nick}" if $reason =~ /^ ?$/;
+ $reason =~ s/^ //;
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ unless (has_local_flag($context, $chatnet, $channel, 'm')) {
+ if ($nick eq $server->{nick}) {
+ $context->{error}("I refuse to kick myself from \cb$channel\cb.");
+ next;
+ }
+ }
+ channel_notice $server, $channel, "$nick was kicked from $channel by $context->{nick} [$reason]";
+ $server->command("kick $channel $nick $reason");
+ }
+}
+
+sub cmd_ban($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re +$masks_re *$/o) {
+ $context->{usage}("ban <#channel> <mask/nick>...");
+ return;
+ }
+ my ($channel, $masks) = (lc $1, $2);
+ my @masks = split ' ', $masks;
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @good = ();
+ foreach my $mask (@masks) {
+ if ($mask !~ /!/) {
+ if ($mask =~ /\@/) {
+ $mask = "*!$mask";
+ } else {
+ my $who = $chan->nick_find($mask);
+ unless ($who) {
+ $context->{error}("\cb$mask\cb is not on \cb$channel\cb.");
+ next;
+ }
+ my $address = $who->{host};
+ if ($address eq '') {
+ $context->{error}("Sorry, I don't know \cb$mask\cb's address yet.");
+ next;
+ }
+ $mask = "*!" . improve_mask $address;
+ }
+ }
+ push @good, $mask;
+ }
+ if (@good) {
+ my $cmd = "+" . "b" x @good . " @good";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ }
+}
+
+sub cmd_unban($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re(?: +$masks_re)? *$/o) {
+ $context->{usage}("unban <#channel> [<masks>]");
+ return;
+ }
+ my ($channel, $masks) = (lc $1, $2);
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ my @masks = ();
+ if (defined $masks) {
+ @masks = split ' ', $masks;
+ } else {
+ my $nick = $context->{nick};
+ my $address = $context->{address};
+ foreach my $ban ($chan->bans()) {
+ push @masks, $ban->{ban}
+ if Irssi::mask_match_address($ban->{ban}, $nick, $address);
+ }
+ unless (@masks) {
+ $context->{notice}("There are no bans against you on \cb$channel\cb.");
+ return;
+ }
+ }
+ my $cmd = "-" . "b" x @masks . " @masks";
+ channel_notice $server, $channel, "mode/$channel [$cmd] by $context->{nick}";
+ $server->command("mode $channel $cmd");
+ unless (defined $masks) {
+ $context->{notice}("Any bans against you on \cb$channel\cb have been cleared.");
+ }
+}
+
+sub cmd_kickban($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ unless ($args =~ /^ *$channel_re +$nicks_commas_re(| .*)$/o) {
+ $context->{usage}("kickban <#channel> <nicks> [<reason>]");
+ return;
+ }
+ my ($channel, $nicks, $reason) = (lc $1, $2, $3);
+ my @nicks = split /,/, $nicks;
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ $reason = " $context->{nick}" if $reason =~ /^ ?$/;
+ $reason =~ s/^ //;
+ foreach my $nick (@nicks) {
+ my $who = $chan->nick_find($nick);
+ unless ($who) {
+ $context->{error}("\cb$nick\cb is not on \cb$channel\cb.");
+ next;
+ }
+ unless (has_local_flag($context, $chatnet, $channel, 'm')) {
+ if ($nick eq $server->{nick}) {
+ $context->{error}("I refuse to kick myself from \cb$channel\cb.");
+ next;
+ }
+ }
+ my $address = $who->{host};
+ if ($address eq '') {
+ $context->{error}("Sorry, I don't know \cb$nick\cb's address yet.");
+ } else {
+ ban $server, $channel, $nick, $address, $$who->{op}, {};
+ }
+ channel_notice $server, $channel, "$nick was kicked from $channel by $context->{nick} [$reason]";
+ $server->command("kick $channel $nick $reason");
+ }
+}
+
+sub cmd_invite($$) {
+ my ($context, $args) = @_;
+ must_be_operator $context or return;
+ my ($channel, $nick);
+ if ($args =~ /^ *$channel_re(?: +$nick_re)? *$/o) {
+ ($channel, $nick) = (lc $1, $2);
+ } elsif ($args =~ /^ *$nick_re +$channel_re *$/o) {
+ ($nick, $channel) = ($1, lc $2);
+ } else {
+ $context->{usage}("invite <#channel> [<nick>]");
+ return;
+ }
+ $nick = $context->{nick} unless defined $nick;
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ must_be_channel_operator $context, $chatnet, $channel or return;
+ my $chan = find_channel $context, $channel, 1 or return;
+ if ($chan->nick_find($nick)) {
+ $context->{error}("\cb$nick\cb is already on \cb$channel\cb");
+ return;
+ }
+ channel_notice $server, "$nick,$channel", "$context->{nick} invited $nick into $channel";
+ $server->command("invite $nick $channel");
+}
+
+######## AUTHENTICATION ########
+
+sub must_have_crypt($) {
+ my ($context) = @_;
+ $context->{error}("Sorry, passwords don't work here - Crypt::PasswdMD5 module not found.")
+ unless $has_crypt;
+ return $has_crypt;
+}
+
+our @salt_chars = ('.', '/', '0'..'9', 'A'..'Z', 'a'..'z');
+
+sub crypt_new_password($) {
+ my ($password) = @_;
+ my $salt = join('', map {$salt_chars[rand @salt_chars]} (1..8));
+ return unix_md5_crypt($password, $salt);
+}
+
+sub check_password($$) {
+ my ($password, $required) = @_;
+ return $required eq unix_md5_crypt($password, $required);
+}
+
+sub cmd_pass($$) {
+ my ($context, $args) = @_;
+ unless ($args =~ /^ *([^ ]+)(?: +([^ ]+))? *$/) {
+ $context->{usage} ("pass <password> - authenticate or set password for the first time");
+ $context->{usage_next}("pass <password> <new-password> - change password");
+ return;
+ }
+ my ($password, $new_password) = ($1, $2);
+ my $server = $context->{server};
+ my $chatnet = lc $server->{chatnet};
+ my $nick = $context->{nick};
+ my $address = $context->{address};
+ my $password_set = 0;
+ my $right_password = 0;
+ my $wrong_password = 0;
+ foreach my $user (find_users undef, $nick, $address) {
+ my ($hdl, $mask) = @$user;
+ my $required = $user_flags{$hdl}{p};
+ next unless defined $required;
+ must_have_crypt $context or return;
+ my $who_nick = "\cc11$nick\co \cc14[\cc10$address\cc14]\co";
+ my $who_hdl = "\cc04$handles{$hdl}\co";
+ if ($required ne '' && !check_password($password, $required)) {
+ print CLIENTNOTICE "$who_nick gave \cbwrong\cb password for $who_hdl.";
+ $wrong_password = 1;
+ next;
+ }
+ if ($required eq '' || defined $new_password) {
+ $password = $new_password if defined $new_password;
+ $user_flags{$hdl}{p} = crypt_new_password $password;
+ print CLIENTNOTICE "$who_nick \cbset\cb the password for $who_hdl.";
+ $password_set = 1;
+ } else {
+ print CLIENTNOTICE "$who_nick gave \cbright\cb password for $who_hdl.";
+ $right_password = 1;
+ }
+ $authenticated{$chatnet}{$address}{$hdl} = 1;
+ maybe_disappears $chatnet, $server, undef, $nick, $address;
+ foreach my $chan ($server->channels()) {
+ next unless $chan->{wholist};
+ next unless $chan->{chanop};
+ my $channel = lc $chan->{name};
+ # nick_find_mask() only returns one nick.
+ foreach my $who (grep { $_->{host} eq $address } $chan->nicks()) {
+ my ($flags, $users) = find_local_flags $chatnet, $channel, $nick, $address;
+ next if defined $flags->{x};
+ if (defined $flags->{r} || defined $flags->{o}) {
+ queue_action $chatnet, '+o', $channel, $who->{nick};
+ }
+ if (defined $flags->{v}) {
+ queue_action $chatnet, '+v', $channel, $who->{nick};
+ }
+ # FIXME: flag +e?
+ }
+ }
+ }
+ if ($password_set || $right_password) {
+ $context->{notice}("Your password has been set.") if $password_set;
+ $context->{notice}("Right password.") if $right_password;
+ } elsif ($wrong_password) {
+ $context->{error}("Wrong password.");
+ } else {
+ $context->{error}("Sorry, I don't recognize you.");
+ }
+ save_config if $password_set;
+}
+
+######## LOCAL COMMANDS ########
+
+Irssi::command_bind 'user', sub {
+ my ($args, $server, $target) = @_;
+ Irssi::command_runsub 'user', $args, $server, $target;
+};
+
+Irssi::command_bind 'mask', sub {
+ my ($args, $server, $target) = @_;
+ Irssi::command_runsub 'mask', $args, $server, $target;
+};
+
+sub local_command($$) {
+ my ($command, $func) = @_;
+ Irssi::command_bind $command, sub {
+ my ($args, $server, $target) = @_;
+ $func->($local_context, $args);
+ };
+ $local_help{$command} = 1;
+}
+
+local_command 'help', \&cmd_help;
+delete $local_help{help};
+local_command 'user add', \&cmd_user_add;
+local_command 'user remove', \&cmd_user_remove;
+local_command 'mask add', \&cmd_mask_add;
+local_command 'mask remove', \&cmd_mask_remove;
+local_command 'user rename', \&cmd_user_rename;
+local_command 'user list', \&cmd_user_list;
+local_command 'flag', \&cmd_flag;
+local_command 'find', \&cmd_find;
+local_command 'trust', \&cmd_trust;
+
+######## RESPOND TO MESSAGES ########
+
+our %commands;
+
+sub run_subcommand($$$) {
+ my ($command, $context, $args) = @_;
+ if ($args =~ / *([a-zA-Z]+)(| .*)$/) {
+ my ($subcommand, $subargs) = ($1, $2);
+ my $func = $commands{"$command " . lc $subcommand} or return;
+ $func->($context, $subargs);
+ }
+}
+
+%commands = (
+ help => \&cmd_help,
+ user => sub {&run_subcommand('user', @_)},
+ mask => sub {&run_subcommand('mask', @_)},
+ 'user add' => \&cmd_user_add,
+ 'user remove' => \&cmd_user_remove,
+ 'mask add' => \&cmd_mask_add,
+ 'mask remove' => \&cmd_mask_remove,
+ 'user rename' => \&cmd_user_rename,
+ 'user list' => \&cmd_user_list,
+ flag => \&cmd_flag,
+ find => \&cmd_find,
+ trust => \&cmd_trust,
+ op => \&cmd_op,
+ deop => \&cmd_deop,
+ voice => \&cmd_voice,
+ devoice => \&cmd_devoice,
+ kick => \&cmd_kick,
+ ban => \&cmd_ban,
+ unban => \&cmd_unban,
+ kickban => \&cmd_kickban,
+ invite => \&cmd_invite,
+ pass => \&cmd_pass,
+);
+
+sub remote_command($$$$$$) {
+ my ($server, $msg, $nick, $address, $reply, $prefix) = @_;
+ return 0 unless $msg =~ /^([a-zA-Z]+)(| .*)$/;
+ my ($command, $args) = ($1, $2);
+ my $func = $commands{lc $command} or return 0;
+ my $chatnet = lc $server->{chatnet};
+ my ($globals, $locals) = find_all_flags $chatnet, $nick, $address;
+ my $context = {
+ crap => sub {$server->command("$reply $nick $_[0]")},
+ notice => sub {$server->command("$reply $nick $_[0]")},
+ error => sub {$server->command("$reply $nick $_[0]")},
+ usage => sub {$server->command("$reply $nick Usage: $prefix$_[0]")},
+ usage_next => sub {$server->command("$reply $nick $prefix$_[0]")},
+ owner => 0,
+ globals => $globals,
+ locals => $locals,
+ set_flags => \%master_set_flags,
+ set_flags_str => $master_set_flags,
+ see_flags => \%master_see_flags,
+ server => $server,
+ nick => $nick,
+ address => $address,
+ };
+ $func->($context, $args);
+ return 1;
+}
+
+Irssi::signal_add_last 'message private', sub {
+ my ($server, $msg, $nick, $address) = @_;
+ return unless $msg =~ /^!(.*)$/;
+ Irssi::signal_continue @_;
+ remote_command $server, $1, $nick, $address, "notice", "!";
+};
+
+Irssi::signal_add_last "ctcp msg", sub {
+ my ($server, $args, $nick, $address, $target) = @_;
+ return unless lc $target eq lc $server->{nick};
+ remote_command $server, $args, $nick, $address, "notice", ""
+ and Irssi::signal_stop;
+};
+
+######## INITIALIZATION ########
+
+load_config;