From f1c5b8c7066f70908692cb74d9cde681e9a63ac7 Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Thu, 24 Jun 2021 18:28:49 -0700 Subject: [PATCH] Progress on refactoring and polishing everything --- PBot/BanList.pm | 8 +- PBot/ChanOpCommands.pm | 4 +- PBot/IRC/Event.pm | 2 +- PBot/IRCHandlers.pm | 446 +++++++++++++++++++++++++++++------------ PBot/Interpreter.pm | 14 +- PBot/NickList.pm | 2 + PBot/PBot.pm | 1 + Plugins/RemindMe.pm | 3 +- 8 files changed, 335 insertions(+), 145 deletions(-) diff --git a/PBot/BanList.pm b/PBot/BanList.pm index d17d0246..f0ecfd42 100644 --- a/PBot/BanList.pm +++ b/PBot/BanList.pm @@ -624,14 +624,14 @@ sub flush_ban_queue { my $target = pop @{$self->{ban_queue}->{$channel}->{$mode}}; $list .= " $target"; $modes .= $mode; - last if ++$count >= $self->{pbot}->{ircd}->{MODES}; + last if ++$count >= $self->{pbot}->{isupport}->{MODES} // 1; } if (not @{$self->{ban_queue}->{$channel}->{$mode}}) { delete $self->{ban_queue}->{$channel}->{$mode}; } - last if $count >= $self->{pbot}->{ircd}->{MODES}; + last if $count >= $self->{pbot}->{isupport}->{MODES} // 1; } if (not keys %{$self->{ban_queue}->{$channel}}) { @@ -675,14 +675,14 @@ sub flush_unban_queue { my $target = pop @{$self->{unban_queue}->{$channel}->{$mode}}; $list .= " $target"; $modes .= $mode; - last if ++$count >= $self->{pbot}->{ircd}->{MODES}; + last if ++$count >= $self->{pbot}->{isupport}->{MODES} // 1; } if (not @{$self->{unban_queue}->{$channel}->{$mode}}) { delete $self->{unban_queue}->{$channel}->{$mode}; } - last if $count >= $self->{pbot}->{ircd}->{MODES}; + last if $count >= $self->{pbot}->{isupport}->{MODES} // 1; } if (not keys %{$self->{unban_queue}->{$channel}}) { diff --git a/PBot/ChanOpCommands.pm b/PBot/ChanOpCommands.pm index 7ca04626..2258fd53 100644 --- a/PBot/ChanOpCommands.pm +++ b/PBot/ChanOpCommands.pm @@ -174,7 +174,7 @@ sub generic_mode { # add $nick to $args if no argument if (not $self->{pbot}->{interpreter}->arglist_size($context->{arglist})) { $self->{pbot}->{interpreter}->unshift_arg($context->{arglist}, $context->{nick}); } - my $max_modes = $self->{pbot}->{ircd}->{MODES} // 1; + my $max_modes = $self->{pbot}->{isupport}->{MODES} // 1; my $mode = $flag; my $list = ''; my $i = 0; @@ -247,7 +247,7 @@ sub cmd_mode { my $arg = 0; my ($new_modes, $new_targets) = ("", ""); - my $max_modes = $self->{pbot}->{ircd}->{MODES} // 1; + my $max_modes = $self->{pbot}->{isupport}->{MODES} // 1; my $u = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask}); diff --git a/PBot/IRC/Event.pm b/PBot/IRC/Event.pm index f5b3c4dd..652d9dba 100644 --- a/PBot/IRC/Event.pm +++ b/PBot/IRC/Event.pm @@ -205,7 +205,7 @@ sub trans { '002' => "yourhost", '003' => "created", '004' => "myinfo", - '005' => "map", # Undernet Extension, Kajetan@Hinner.com, 17/11/98 + '005' => "isupport", # Undernet Extension, Kajetan@Hinner.com, 17/11/98 # renamed to `isupport` - pragma June 24, 2021 '006' => "mapmore", # Undernet Extension, Kajetan@Hinner.com, 17/11/98 '007' => "mapend", # Undernet Extension, Kajetan@Hinner.com, 17/11/98 '008' => "snomask", # Undernet Extension, Kajetan@Hinner.com, 17/11/98 diff --git a/PBot/IRCHandlers.pm b/PBot/IRCHandlers.pm index 23e6c686..2470ffeb 100644 --- a/PBot/IRCHandlers.pm +++ b/PBot/IRCHandlers.pm @@ -1,6 +1,9 @@ # File: IRCHandlers.pm # -# Purpose: Subroutines to handle IRC events +# Purpose: Subroutines to handle IRC events. Note that various PBot packages +# can in turn register their own IRC event handlers as well. There can be +# multiple handlers for PRIVMSG spread throughout the bot and its plugins, +# for example. # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,85 +14,107 @@ use parent 'PBot::Class'; use PBot::Imports; -use Time::HiRes qw(gettimeofday); +use Time::HiRes qw/time/; use Data::Dumper; use MIME::Base64; use Encode; -$Data::Dumper::Sortkeys = 1; - sub initialize { my ($self, %conf) = @_; - $self->{pbot}->{event_dispatcher}->register_handler('irc.welcome', sub { $self->on_connect(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.disconnect', sub { $self->on_disconnect(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.motd', sub { $self->on_motd(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.notice', sub { $self->on_notice(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.public', sub { $self->on_public(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.caction', sub { $self->on_action(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.msg', sub { $self->on_msg(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.mode', sub { $self->on_mode(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.part', sub { $self->on_departure(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_departure(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.nick', sub { $self->on_nickchange(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.nicknameinuse', sub { $self->on_nicknameinuse(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.invite', sub { $self->on_invite(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.map', sub { $self->on_map(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.whoreply', sub { $self->on_whoreply(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.whospcrpl', sub { $self->on_whospcrpl(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.endofwho', sub { $self->on_endofwho(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.channelmodeis', sub { $self->on_channelmodeis(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.topic', sub { $self->on_topic(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.topicinfo', sub { $self->on_topicinfo(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.channelcreate', sub { $self->on_channelcreate(@_) }); + + # convenient alias so the following lings aren't so long + my $ed = $self->{pbot}->{event_dispatcher}; + + # various IRC events (note that other PBot packages and plugins can + # also register additional IRC event handlers, including handlers for + # the events listed here. any duplicate events will be chained.) + $ed->register_handler('irc.welcome', sub { $self->on_connect (@_) }); + $ed->register_handler('irc.disconnect', sub { $self->on_disconnect (@_) }); + $ed->register_handler('irc.motd', sub { $self->on_motd (@_) }); + $ed->register_handler('irc.notice', sub { $self->on_notice (@_) }); + $ed->register_handler('irc.public', sub { $self->on_public (@_) }); + $ed->register_handler('irc.caction', sub { $self->on_action (@_) }); + $ed->register_handler('irc.msg', sub { $self->on_msg (@_) }); + $ed->register_handler('irc.mode', sub { $self->on_mode (@_) }); + $ed->register_handler('irc.part', sub { $self->on_departure (@_) }); + $ed->register_handler('irc.join', sub { $self->on_join (@_) }); + $ed->register_handler('irc.kick', sub { $self->on_kick (@_) }); + $ed->register_handler('irc.quit', sub { $self->on_departure (@_) }); + $ed->register_handler('irc.nick', sub { $self->on_nickchange (@_) }); + $ed->register_handler('irc.nicknameinuse', sub { $self->on_nicknameinuse (@_) }); + $ed->register_handler('irc.invite', sub { $self->on_invite (@_) }); + $ed->register_handler('irc.isupport', sub { $self->on_isupport (@_) }); + $ed->register_handler('irc.whoreply', sub { $self->on_whoreply (@_) }); + $ed->register_handler('irc.whospcrpl', sub { $self->on_whospcrpl (@_) }); + $ed->register_handler('irc.endofwho', sub { $self->on_endofwho (@_) }); + $ed->register_handler('irc.channelmodeis', sub { $self->on_channelmodeis (@_) }); + $ed->register_handler('irc.topic', sub { $self->on_topic (@_) }); + $ed->register_handler('irc.topicinfo', sub { $self->on_topicinfo (@_) }); + $ed->register_handler('irc.channelcreate', sub { $self->on_channelcreate (@_) }); + $ed->register_handler('irc.yourhost', sub { $self->log_first_arg (@_) }); + $ed->register_handler('irc.created', sub { $self->log_first_arg (@_) }); + $ed->register_handler('irc.luserconns', sub { $self->log_first_arg (@_) }); + $ed->register_handler('irc.notregistered', sub { $self->log_first_arg (@_) }); + $ed->register_handler('irc.n_local', sub { $self->log_third_arg (@_) }); + $ed->register_handler('irc.n_global', sub { $self->log_third_arg (@_) }); # IRCv3 client capabilities - $self->{pbot}->{event_dispatcher}->register_handler('irc.cap', sub { $self->on_cap(@_) }); + $ed->register_handler('irc.cap', sub { $self->on_cap(@_) }); # IRCv3 SASL - $self->{pbot}->{event_dispatcher}->register_handler('irc.authenticate', sub { $self->on_sasl_authenticate(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.rpl_loggedin', sub { $self->on_rpl_loggedin(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.rpl_loggedout', sub { $self->on_rpl_loggedout(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.err_nicklocked', sub { $self->on_err_nicklocked(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.rpl_saslsuccess', sub { $self->on_rpl_saslsuccess(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.err_saslfail', sub { $self->on_err_saslfail(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.err_sasltoolong', sub { $self->on_err_sasltoolong(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.err_saslaborted', sub { $self->on_err_saslaborted(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.err_saslalready', sub { $self->on_err_saslalready(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('irc.rpl_saslmechs', sub { $self->on_rpl_saslmechs(@_) }); + $ed->register_handler('irc.authenticate', sub { $self->on_sasl_authenticate (@_) }); + $ed->register_handler('irc.rpl_loggedin', sub { $self->on_rpl_loggedin (@_) }); + $ed->register_handler('irc.rpl_loggedout', sub { $self->on_rpl_loggedout (@_) }); + $ed->register_handler('irc.err_nicklocked', sub { $self->on_err_nicklocked (@_) }); + $ed->register_handler('irc.rpl_saslsuccess', sub { $self->on_rpl_saslsuccess (@_) }); + $ed->register_handler('irc.err_saslfail', sub { $self->on_err_saslfail (@_) }); + $ed->register_handler('irc.err_sasltoolong', sub { $self->on_err_sasltoolong (@_) }); + $ed->register_handler('irc.err_saslaborted', sub { $self->on_err_saslaborted (@_) }); + $ed->register_handler('irc.err_saslalready', sub { $self->on_err_saslalready (@_) }); + $ed->register_handler('irc.rpl_saslmechs', sub { $self->on_rpl_saslmechs (@_) }); # bot itself joining and parting channels - $self->{pbot}->{event_dispatcher}->register_handler('pbot.join', sub { $self->on_self_join(@_) }); - $self->{pbot}->{event_dispatcher}->register_handler('pbot.part', sub { $self->on_self_part(@_) }); + $ed->register_handler('pbot.join', sub { $self->on_self_join(@_) }); + $ed->register_handler('pbot.part', sub { $self->on_self_part(@_) }); # TODO: enqueue these events as needed instead of naively checking every 10 seconds $self->{pbot}->{event_queue}->enqueue(sub { $self->check_pending_whos }, 10, 'Check pending WHOs'); } +# default PBot::IRC handler. this handler prepends 'irc.' to the event-type +# and then dispatches the event through PBot::EventDispatcher sub default_handler { my ($self, $conn, $event) = @_; - if (not defined $self->{pbot}->{event_dispatcher}->dispatch_event("irc.$event->{type}", {conn => $conn, event => $event})) { - if ($self->{pbot}->{registry}->get_value('irc', 'log_default_handler')) { $self->{pbot}->{logger}->log(Dumper $event); } + my $result = $self->{pbot}->{event_dispatcher}->dispatch_event( + "irc.$event->{type}", + { + conn => $conn, + event => $event + } + ); + + if (not defined $result and $self->{pbot}->{registry}->get_value('irc', 'log_default_handler')) { + $Data::Dumper::Sortkeys = 1; + $self->{pbot}->{logger}->log(Dumper $event); } } sub on_init { my ($self, $conn, $event) = @_; my (@args) = ($event->args); - shift(@args); + shift @args; $self->{pbot}->{logger}->log("*** @args\n"); } sub on_connect { my ($self, $event_type, $event) = @_; + $self->{pbot}->{logger}->log("Connected!\n"); - $event->{conn}->{connected} = 1; if (not $self->{pbot}->{irc_capabilities}->{sasl}) { - # not using SASL, so identify the old way by /msg NickServ or some bot + # not using SASL, so identify the old way by /msging NickServ or some such services bot if (length $self->{pbot}->{registry}->get_value('irc', 'identify_password')) { $self->{pbot}->{logger}->log("Identifying with NickServ . . .\n"); @@ -104,9 +129,11 @@ sub on_connect { $event->{conn}->privmsg($nickserv, $command); } else { + # using SASL, we're already identified at this point $self->{pbot}->{logger}->log("No identify password; skipping identification to services.\n"); } + # auto-join channels unless general.autojoin_wait_for_nickserv is true if (not $self->{pbot}->{registry}->get_value('general', 'autojoin_wait_for_nickserv')) { $self->{pbot}->{logger}->log("Autojoining channels immediately; to wait for services set general.autojoin_wait_for_nickserv to 1.\n"); $self->{pbot}->{channels}->autojoin; @@ -114,7 +141,7 @@ sub on_connect { $self->{pbot}->{logger}->log("Waiting for services identify response before autojoining channels.\n"); } } else { - # using SASL; go ahead and auto-join channels + # using SASL; go ahead and auto-join channels now $self->{pbot}->{logger}->log("Autojoining channels.\n"); $self->{pbot}->{channels}->autojoin; } @@ -124,9 +151,14 @@ sub on_connect { sub on_disconnect { my ($self, $event_type, $event) = @_; + $self->{pbot}->{logger}->log("Disconnected...\n"); $self->{pbot}->{connected} = 0; + + # attempt to reconnect to server + # TODO: maybe add a registry entry to control whether the bot auto-reconnects $self->{pbot}->connect; + return 0; } @@ -134,117 +166,168 @@ sub on_motd { my ($self, $event_type, $event) = @_; if ($self->{pbot}->{registry}->get_value('irc', 'show_motd')) { - my $server = $event->{event}->{from}; - my $msg = $event->{event}->{args}[1]; - $self->{pbot}->{logger}->log("MOTD from $server :: $msg\n"); + my $from = $event->{event}->{from}; + my $msg = $event->{event}->{args}->[1]; + $self->{pbot}->{logger}->log("MOTD from $from :: $msg\n"); } + return 0; } +# the bot itself joining a channel sub on_self_join { my ($self, $event_type, $event) = @_; + # early-return if we don't send WHO on join + # (we send WHO to see who is in the channel, for ban-evasion enforcement and such) return 0 if not $self->{pbot}->{registry}->get_value('general', 'send_who_on_join') // 1; + # we turn on send_who if the following conditions are met my $send_who = 0; + if ($self->{pbot}->{registry}->get_value('general', 'send_who_chanop_only') // 1) { + # check if we only send WHO to where we can gain ops if ($self->{pbot}->{channels}->get_meta($event->{channel}, 'chanop')) { + # yup, we can +o in this channel, turn on send_who $send_who = 1; } } else { + # otherwise just go ahead turn on send_who $send_who = 1; } + # schedule the WHO to be sent to this channel $self->send_who($event->{channel}) if $send_who; + return 0; } +# the bot itself leaving a channel sub on_self_part { my ($self, $event_type, $event) = @_; + # nothing to do here yet return 0; } sub on_public { my ($self, $event_type, $event) = @_; - my $from = $event->{event}->{to}[0]; - my $nick = $event->{event}->nick; - my $user = $event->{event}->user; - my $host = $event->{event}->host; - my $text = $event->{event}->{args}[0]; + my ($from, $nick, $user, $host, $text) = ( + $event->{event}->{to}->[0], + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->{args}->[0], + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); + # send text to be processed for bot commands, anti-flood enforcement, etc $event->{interpreted} = $self->{pbot}->{interpreter}->process_line($from, $nick, $user, $host, $text); + return 0; } sub on_msg { my ($self, $event_type, $event) = @_; - my ($nick, $host) = ($event->{event}->nick, $event->{event}->host); - my $text = $event->{event}->{args}[0]; - my $bot_trigger = $self->{pbot}->{registry}->get_value('general', 'trigger'); - my $bot_nick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); + my ($nick, $user, $host, $text) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->{args}->[0], + ); + + ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); + + # send text to be processed as a bot command, coming from $nick + $event->{interpreted} = $self->{pbot}->{interpreter}->process_line($nick, $nick, $user, $host, $text, 1); - $text =~ s/^$bot_trigger?\s*(.*)/$bot_nick $1/; - $event->{event}->{to}[0] = $nick; - $event->{event}->{args}[0] = $text; - $self->on_public($event_type, $event); return 0; } sub on_notice { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host); - my $text = $event->{event}->{args}[0]; - $self->{pbot}->{logger}->log("Received NOTICE from $nick!$user\@$host to $event->{event}->{to}[0] '$text'\n"); + my ($nick, $user, $host, $to, $text) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->to, + $event->{event}->{args}->[0], + ); - return 0 if not length $host; + # log notice + $self->{pbot}->{logger}->log("NOTICE from $nick!$user\@$host to $to: $text\n"); + # notice from NickServ if ($nick eq 'NickServ') { + # if we have enabled NickServ GUARD protection and we're not identified yet, + # NickServ will warn us to identify -- this is our cue to identify. if ($text =~ m/This nickname is registered/) { if (length $self->{pbot}->{registry}->get_value('irc', 'identify_password')) { $self->{pbot}->{logger}->log("Identifying with NickServ . . .\n"); $event->{conn}->privmsg("nickserv", "identify " . $self->{pbot}->{registry}->get_value('irc', 'identify_password')); } - } elsif ($text =~ m/You are now identified/) { - if ($self->{pbot}->{registry}->get_value('irc', 'randomize_nick')) { $event->{conn}->nick($self->{pbot}->{registry}->get_value('irc', 'botnick')); } - else { $self->{pbot}->{channels}->autojoin; } - } elsif ($text =~ m/has been ghosted/) { + } + elsif ($text =~ m/You are now identified/) { + # we have identified with NickServ + if ($self->{pbot}->{registry}->get_value('irc', 'randomize_nick')) { + # if irc.randomize_nicks was enabled, we go ahead and attempt to + # change to our real botnick. we don't auto-join channels just yet in case + # the nick change fails. + $event->{conn}->nick($self->{pbot}->{registry}->get_value('irc', 'botnick')); + } else { + # otherwise go ahead and autojoin channels now + $self->{pbot}->{channels}->autojoin; + } + } + elsif ($text =~ m/has been ghosted/) { + # we have ghosted someone using our botnick, let's attempt to regain it now $event->{conn}->nick($self->{pbot}->{registry}->get_value('irc', 'botnick')); } } else { - if ($event->{event}->{to}[0] eq $self->{pbot}->{registry}->get_value('irc', 'botnick')) { $event->{event}->{to}[0] = $nick; } - $self->on_public($event_type, $event); + # if NOTICE is sent to the bot then replace the `to` field with the + # sender's nick instead so when we pass it on to on_public ... + if ($to eq $self->{pbot}->{registry}->get_value('irc', 'botnick')) { + $event->{event}->{to}->[0] = $nick; + } + + # handle this NOTICE as a public message + # (check for bot commands, anti-flooding, etc) + $self->on_public($event_type, $event) unless $to eq '*'; } + return 0; } sub on_action { my ($self, $event_type, $event) = @_; - $event->{event}->{args}[0] = "/me " . $event->{event}->{args}[0]; + # prepend "/me " to the message text + $event->{event}->{args}->[0] = "/me " . $event->{event}->{args}->[0]; + # pass this along to on_public $self->on_public($event_type, $event); return 0; } # FIXME: on_mode doesn't handle chanmodes that have parameters, e.g. +l - sub on_mode { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host); - my $mode_string = $event->{event}->{args}[0]; - my $channel = $event->{event}->{to}[0]; - $channel = lc $channel; + + my ($nick, $user, $host, $mode_string, $channel) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->{args}->[0], + lc $event->{event}->{to}->[0], + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); - my ($mode, $mode_char, $modifier); my $i = 0; - my $target; + my ($mode, $mode_char, $modifier, $target); while ($mode_string =~ m/(.)/g) { my $char = $1; @@ -254,32 +337,41 @@ sub on_mode { next; } - $mode = $modifier . $char; - $mode_char = $char; - $target = $event->{event}->{args}[++$i]; + $mode = $modifier . $char; + $target = $event->{event}->{args}->[++$i]; $self->{pbot}->{logger}->log("Mode $channel [$mode" . (length $target ? " $target" : '') . "] by $nick!$user\@$host\n"); + # TODO: figure out a good way to allow other packages to receive "track_mode" events + # i.e., perhaps by emitting a modechange event or some such and registering handlers $self->{pbot}->{banlist}->track_mode("$nick!$user\@$host", $channel, $mode, $target); $self->{pbot}->{chanops}->track_mode("$nick!$user\@$host", $channel, $mode, $target); if (defined $target and length $target) { # mode set on user my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host); + $self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, "MODE $mode $target", $self->{pbot}->{messagehistory}->{MSG_CHAT}); - if ($modifier eq '-') { $self->{pbot}->{nicklist}->delete_meta($channel, $target, "+$mode_char"); } - else { $self->{pbot}->{nicklist}->set_meta($channel, $target, $mode, 1); } + # TODO: here as well + if ($modifier eq '-') { + $self->{pbot}->{nicklist}->delete_meta($channel, $target, "+$mode_char"); + } else { + $self->{pbot}->{nicklist}->set_meta($channel, $target, $mode, 1); + } } else { # mode set on channel my $modes = $self->{pbot}->{channels}->get_meta($channel, 'MODE'); + if (defined $modes) { if ($modifier eq '+') { $modes = '+' if not length $modes; $modes .= $mode_char; } else { - $modes =~ s/\Q$mode_char\E//g; + $modes =~ s/\Q$mode_char//g; } + + # TODO: here as well $self->{pbot}->{channels}->{channels}->set($channel, 'MODE', $modes, 1); } } @@ -290,12 +382,16 @@ sub on_mode { sub on_join { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to); + + my ($nick, $user, $host, $channel) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + lc $event->{event}->{to}->[0], + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); - $channel = lc $channel; - my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host); $self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, "JOIN", $self->{pbot}->{messagehistory}->{MSG_JOIN}); @@ -303,14 +399,20 @@ sub on_join { my $msg = 'JOIN'; + # IRCv3 extended-join capability provides more details about user if (exists $self->{pbot}->{irc_capabilities}->{'extended-join'}) { - $msg .= " $event->{event}->{args}[0] :$event->{event}->{args}[1]"; + my ($nickserv, $gecos) = ( + $event->{event}->{args}->[0], + $event->{event}->{args}->[1], + ); - $self->{pbot}->{messagehistory}->{database}->update_gecos($message_account, $event->{event}->{args}[1], scalar gettimeofday); + $msg .= " $nickserv :$gecos"; - if ($event->{event}->{args}[0] ne '*') { - $self->{pbot}->{messagehistory}->{database}->link_aliases($message_account, undef, $event->{event}->{args}[0]); - $self->{pbot}->{antiflood}->check_nickserv_accounts($nick, $event->{event}->{args}[0]); + $self->{pbot}->{messagehistory}->{database}->update_gecos($message_account, $gecos, scalar time); + + if ($nickserv ne '*') { + $self->{pbot}->{messagehistory}->{database}->link_aliases($message_account, undef, $nickserv); + $self->{pbot}->{antiflood}->check_nickserv_accounts($nick, $nickserv); } else { $self->{pbot}->{messagehistory}->{database}->set_current_nickserv_account($message_account, ''); } @@ -324,21 +426,30 @@ sub on_join { $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_time_threshold'), $self->{pbot}->{messagehistory}->{MSG_JOIN} ); + return 0; } sub on_invite { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host, $target, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to, $event->{event}->{args}[0]); + + my ($nick, $user, $host, $target, $channel) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->to, + lc $event->{event}->{args}->[0] + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); - $channel = lc $channel; - $self->{pbot}->{logger}->log("$nick!$user\@$host invited $target to $channel!\n"); + # if invited to a channel on our channel list, go ahead and join it if ($target eq $self->{pbot}->{registry}->get_value('irc', 'botnick')) { - if ($self->{pbot}->{channels}->is_active($channel)) { $self->{pbot}->{interpreter}->add_botcmd_to_command_queue($channel, "join $channel", 0); } + if ($self->{pbot}->{channels}->is_active($channel)) { + $self->{pbot}->{interpreter}->add_botcmd_to_command_queue($channel, "join $channel", 0); + } } return 0; @@ -346,24 +457,38 @@ sub on_invite { sub on_kick { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host, $target, $channel, $reason) = - ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to, $event->{event}->{args}[0], $event->{event}->{args}[1]); - $channel = lc $channel; + + my ($nick, $user, $host, $target, $channel, $reason) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + $event->{event}->to, + lc $event->{event}->{args}->[0], + $event->{event}->{args}->[1] + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); $self->{pbot}->{logger}->log("$nick!$user\@$host kicked $target from $channel ($reason)\n"); + # hostmask of the person being kicked + my $target_hostmask; + + # look up message history account for person being kicked my ($message_account) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($target); - my $hostmask; if (defined $message_account) { - $hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($message_account); + # update target hostmask + $target_hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($message_account); - my ($target_nick, $target_user, $target_host) = $hostmask =~ m/^([^!]+)!([^@]+)@(.*)/; + # add "KICKED by" to kicked person's message history my $text = "KICKED by $nick!$user\@$host ($reason)"; - $self->{pbot}->{messagehistory}->add_message($message_account, $hostmask, $channel, $text, $self->{pbot}->{messagehistory}->{MSG_DEPARTURE}); + $self->{pbot}->{messagehistory}->add_message($message_account, $target_hostmask, $channel, $text, $self->{pbot}->{messagehistory}->{MSG_DEPARTURE}); + + # do stuff that happens in check_flood + my ($target_nick, $target_user, $target_host) = $target_hostmask =~ m/^([^!]+)!([^@]+)@(.*)/; + $self->{pbot}->{antiflood}->check_flood( $channel, $target_nick, $target_user, $target_host, $text, $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_threshold'), @@ -372,29 +497,42 @@ sub on_kick { ); } + # look up message history account for person doing the kicking $message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account_id("$nick!$user\@$host"); if (defined $message_account) { - my $text = "KICKED " . (defined $hostmask ? $hostmask : $target) . " from $channel ($reason)"; + # replace target nick with target hostmask if available + if (defined $target_hostmask) { + $target = $target_hostmask; + } + + # add "KICKED $target" to kicker's message history + my $text = "KICKED $target from $channel ($reason)"; $self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, $text, $self->{pbot}->{messagehistory}->{MSG_CHAT}); } + return 0; } sub on_departure { my ($self, $event_type, $event) = @_; - my ($nick, $user, $host, $channel, $args) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to, $event->{event}->args); - $channel = lc $channel; + + my ($nick, $user, $host, $channel, $args) = ( + $event->{event}->nick, + $event->{event}->user, + $event->{event}->host, + lc $event->{event}->{to}->[0], + $event->{event}->args + ); ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); - my $text = uc $event->{event}->type; - $text .= " $args"; + my $text = uc ($event->{event}->type) . $args; my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host); if ($text =~ m/^QUIT/) { - # QUIT messages must be dispatched to each channel the user is on + # QUIT messages must be added to the mesasge history of each channel the user is on my $channels = $self->{pbot}->{nicklist}->get_channels($nick); foreach my $chan (@$channels) { next if $chan !~ m/^#/; @@ -412,32 +550,44 @@ sub on_departure { ); my $u = $self->{pbot}->{users}->find_user($channel, "$nick!$user\@$host"); + + # log user out if logged in and not stayloggedin + # TODO: this should probably be in Users.pm with its own part/quit/kick handler if (defined $u and $u->{loggedin} and not $u->{stayloggedin}) { $self->{pbot}->{logger}->log("Logged out $nick.\n"); delete $u->{loggedin}; $self->{pbot}->{users}->save; } + return 0; } -sub on_map { +sub on_isupport { my ($self, $event_type, $event) = @_; - # remove and discard first and last elements + # remove and discard first and last arguments + # (first arg is botnick, last arg is "are supported by this server") shift @{$event->{event}->{args}}; - pop @{$event->{event}->{args}}; + pop @{$event->{event}->{args}}; + + my $logmsg = "$event->{event}->{from} supports:"; foreach my $arg (@{$event->{event}->{args}}) { my ($key, $value) = split /=/, $arg; - $self->{pbot}->{ircd}->{$key} = $value; - - if (not defined $value) { - $self->{pbot}->{logger}->log(" $key\n"); + if ($key =~ /^-/) { + # server removed suppport for this key + delete $self->{pbot}->{isupport}->{$key}; } else { - $self->{pbot}->{logger}->log(" $key=$value\n"); + $self->{pbot}->{isupport}->{$key} = $value // 1; } + + $logmsg .= defined $value ? " $key=$value" : " $key"; } + + $self->{pbot}->{logger}->log("$logmsg\n"); + + return 0; } # IRCv3 client capability negotiation @@ -522,7 +672,7 @@ sub on_cap { if ($cap eq 'sasl') { # begin SASL authentication # TODO: for now we support only PLAIN - $self->{pbot}->{logger}->log("Performing SASL authentication [PLAIN]\n"); + $self->{pbot}->{logger}->log("Performing SASL authentication PLAIN\n"); $event->{conn}->sl("AUTHENTICATE PLAIN"); } } @@ -532,6 +682,7 @@ sub on_cap { } else { $self->{pbot}->{logger}->log("Unknown CAP event:\n"); + $Data::Dumper::Sortkeys = 1; $self->{pbot}->{logger}->log(Dumper $event->{event}); } @@ -648,11 +799,11 @@ sub on_nickchange { next if $channel !~ m/^#/; $self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, "NICKCHANGE $newnick", $self->{pbot}->{messagehistory}->{MSG_NICKCHANGE}); } - $self->{pbot}->{messagehistory}->{database}->update_hostmask_data("$nick!$user\@$host", {last_seen => scalar gettimeofday}); + $self->{pbot}->{messagehistory}->{database}->update_hostmask_data("$nick!$user\@$host", {last_seen => scalar time}); my $newnick_account = $self->{pbot}->{messagehistory}->{database}->get_message_account($newnick, $user, $host, $nick); $self->{pbot}->{messagehistory}->{database}->devalidate_all_channels($newnick_account, $self->{pbot}->{antiflood}->{NEEDS_CHECKBAN}); - $self->{pbot}->{messagehistory}->{database}->update_hostmask_data("$newnick!$user\@$host", {last_seen => scalar gettimeofday}); + $self->{pbot}->{messagehistory}->{database}->update_hostmask_data("$newnick!$user\@$host", {last_seen => scalar time}); $self->{pbot}->{antiflood}->check_flood( "$nick!$user\@$host", $nick, $user, $host, "NICKCHANGE $newnick", @@ -666,25 +817,35 @@ sub on_nickchange { sub on_nicknameinuse { my ($self, $event_type, $event) = @_; - my (undef, $nick, $msg) = $event->{event}->args; + + my (undef, $nick, $msg) = $event->{event}->args; my $from = $event->{event}->from; $self->{pbot}->{logger}->log("Received nicknameinuse for nick $nick from $from: $msg\n"); + + # attempt to use NickServ GHOST command to kick nick off $event->{conn}->privmsg("nickserv", "ghost $nick " . $self->{pbot}->{registry}->get_value('irc', 'identify_password')); + return 0; } sub on_channelmodeis { my ($self, $event_type, $event) = @_; - my (undef, $channel, $modes) = $event->{event}->args; + + my (undef, $channel, $modes) = $event->{event}->args; + $self->{pbot}->{logger}->log("Channel $channel modes: $modes\n"); + $self->{pbot}->{channels}->{channels}->set($channel, 'MODE', $modes, 1); } sub on_channelcreate { - my ($self, $event_type, $event) = @_; - my ($owner, $channel, $timestamp) = $event->{event}->args; + my ($self, $event_type, $event) = @_; + + my ($owner, $channel, $timestamp) = $event->{event}->args; + $self->{pbot}->{logger}->log("Channel $channel created by $owner on " . localtime($timestamp) . "\n"); + $self->{pbot}->{channels}->{channels}->set($channel, 'CREATED_BY', $owner, 1); $self->{pbot}->{channels}->{channels}->set($channel, 'CREATED_ON', $timestamp, 1); } @@ -706,8 +867,10 @@ sub on_topic { $self->{pbot}->{logger}->log("$nick!$user\@$host changed topic for $channel to: $topic\n"); $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC', $topic, 1); $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC_SET_BY', "$nick!$user\@$host", 1); - $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC_SET_ON', gettimeofday); + $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC_SET_ON', time); } + + return 0; } sub on_topicinfo { @@ -716,6 +879,19 @@ sub on_topicinfo { $self->{pbot}->{logger}->log("Topic for $channel set by $by on " . localtime($timestamp) . "\n"); $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC_SET_BY', $by, 1); $self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC_SET_ON', $timestamp, 1); + return 0; +} + +sub log_first_arg { + my ($self, $event_type, $event) = @_; + $self->{pbot}->{logger}->log("$event->{event}->{args}->[1]\n"); + return 0; +} + +sub log_third_arg { + my ($self, $event_type, $event) = @_; + $self->{pbot}->{logger}->log("$event->{event}->{args}->[3]\n"); + return 0; } sub normalize_hostmask { @@ -736,8 +912,10 @@ my $who_pending = 0; sub on_whoreply { my ($self, $event_type, $event) = @_; - my (undef, $id, $user, $host, $server, $nick, $usermodes, $gecos) = @{$event->{event}->{args}}; - ($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host); + my (undef, $id, $user, $host, $server, $nick, $usermodes, $gecos) = $event->{event}->args; + + ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); + my $hostmask = "$nick!$user\@$host"; my $channel; @@ -767,7 +945,7 @@ sub on_whoreply { $self->{pbot}->{nicklist}->set_meta($channel, $nick, 'gecos', $gecos); my $account_id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); - $self->{pbot}->{messagehistory}->{database}->update_hostmask_data($hostmask, {last_seen => scalar gettimeofday}); + $self->{pbot}->{messagehistory}->{database}->update_hostmask_data($hostmask, {last_seen => scalar time}); $self->{pbot}->{messagehistory}->{database}->link_aliases($account_id, $hostmask, undef); @@ -780,8 +958,10 @@ sub on_whoreply { sub on_whospcrpl { my ($self, $event_type, $event) = @_; - my (undef, $id, $user, $host, $nick, $nickserv, $gecos) = @{$event->{event}->{args}}; - ($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host); + my (undef, $id, $user, $host, $nick, $nickserv, $gecos) = $event->{event}->args; + + ($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host); + $last_who_id = $id; my $hostmask = "$nick!$user\@$host"; my $channel = $who_cache{$id}; @@ -799,7 +979,7 @@ sub on_whospcrpl { $self->{pbot}->{nicklist}->set_meta($channel, $nick, 'gecos', $gecos); my $account_id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); - $self->{pbot}->{messagehistory}->{database}->update_hostmask_data($hostmask, {last_seen => scalar gettimeofday}); + $self->{pbot}->{messagehistory}->{database}->update_hostmask_data($hostmask, {last_seen => scalar time}); if ($nickserv ne '0') { $self->{pbot}->{messagehistory}->{database}->link_aliases($account_id, undef, $nickserv); diff --git a/PBot/Interpreter.pm b/PBot/Interpreter.pm index 143b36b5..6ecfb4bd 100644 --- a/PBot/Interpreter.pm +++ b/PBot/Interpreter.pm @@ -37,7 +37,7 @@ sub initialize { # this is the main entry point for a message to be parsed into commands # and to execute those commands and process their output sub process_line { - my ($self, $from, $nick, $user, $host, $text) = @_; + my ($self, $from, $nick, $user, $host, $text, $is_command) = @_; # lowercase `from` field for case-insensitivity $from = lc $from; @@ -104,9 +104,17 @@ sub process_line { my $command; # current command being parsed my $embedded = 0; # was command embedded within a message, e.g.: "see the !{help xyz} about that" - my $nick_prefix = undef; # addressed nickname for prefixing output - my $processed = 0; # counts how many commands were successfully processed + my $nick_prefix = undef; # addressed nickname for prefixing output + my $processed = 0; # counts how many commands were successfully processed + # check if we should treat this entire text as a command + # (i.e., it came from /msg or was otherwise flagged as a command) + if ($is_command) { + $command = $cmd_text; + goto CHECK_EMBEDDED_CMD; + } + + # otherwise try to parse any potential commands if ($cmd_text =~ m/^\s*($nick_regex)[,:]?\s+$bot_trigger\{\s*(.+?)\s*\}\s*$/) { # "somenick: !{command}" goto CHECK_EMBEDDED_CMD; diff --git a/PBot/NickList.pm b/PBot/NickList.pm index e6e6db8a..efb1b0bc 100644 --- a/PBot/NickList.pm +++ b/PBot/NickList.pm @@ -36,6 +36,8 @@ sub initialize { # handlers for various IRC events # TODO: track mode changes to update user flags + # Update: turns out that IRCHandler's on_mode() is doing this already -- we need to make that + # emit a mode-change event or some such and register a handler for it here. $self->{pbot}->{event_dispatcher}->register_handler('irc.namreply', sub { $self->on_namreply(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.part', sub { $self->on_part(@_) }); diff --git a/PBot/PBot.pm b/PBot/PBot.pm index 57bd967f..43740c7b 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -274,6 +274,7 @@ sub connect { # ignore these events $self->{conn}->add_handler( [ + 'myinfo', 'whoisserver', 'whoiscountry', 'whoischannels', diff --git a/Plugins/RemindMe.pm b/Plugins/RemindMe.pm index 91e0fe92..7226bba8 100644 --- a/Plugins/RemindMe.pm +++ b/Plugins/RemindMe.pm @@ -185,8 +185,7 @@ sub cmd_remindme { # add to the reminder text anything left in the arguments if (@opt_args) { - $text .= ' ' if length $text; - $text .= join ' ', @opt_args; + $text .= " @opt_args"; } return "Please use -t to specify a time for this reminder." if not $alarm;