pbot/PBot/IRCHandlers.pm

853 lines
34 KiB
Perl
Raw Normal View History

# File: IRCHandlers.pm
#
# Purpose: Subroutines to handle IRC events
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
# 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
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::IRCHandlers;
use parent 'PBot::Class';
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2019-07-11 03:40:53 +02:00
use Time::HiRes qw(gettimeofday);
use Data::Dumper;
2020-02-15 23:38:32 +01:00
use MIME::Base64;
use Encode;
2018-02-28 20:13:56 +01:00
$Data::Dumper::Sortkeys = 1;
sub initialize {
2020-02-15 23:38:32 +01:00
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(@_) });
# IRCv3 client capabilities
$self->{pbot}->{event_dispatcher}->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(@_) });
# bot itself joining and parting channels
2020-02-15 23:38:32 +01:00
$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(@_) });
# 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');
}
sub default_handler {
2020-02-15 23:38:32 +01:00
my ($self, $conn, $event) = @_;
2020-02-15 23:38:32 +01:00
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); }
}
}
sub on_init {
2020-02-15 23:38:32 +01:00
my ($self, $conn, $event) = @_;
my (@args) = ($event->args);
shift(@args);
$self->{pbot}->{logger}->log("*** @args\n");
}
sub on_connect {
2020-02-15 23:38:32 +01:00
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
if (length $self->{pbot}->{registry}->get_value('irc', 'identify_password')) {
$self->{pbot}->{logger}->log("Identifying with NickServ . . .\n");
my $nickserv = $self->{pbot}->{registry}->get_value('general', 'identify_nick') // 'nickserv';
my $command = $self->{pbot}->{registry}->get_value('general', 'identify_command') // 'identify $nick $password';
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
my $password = $self->{pbot}->{registry}->get_value('irc', 'identify_password');
$command =~ s/\$nick\b/$botnick/g;
$command =~ s/\$password\b/$password/g;
$event->{conn}->privmsg($nickserv, $command);
} else {
$self->{pbot}->{logger}->log("No identify password; skipping identification to services.\n");
}
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;
} else {
$self->{pbot}->{logger}->log("Waiting for services identify response before autojoining channels.\n");
}
2020-02-15 23:38:32 +01:00
} else {
# using SASL; go ahead and auto-join channels
$self->{pbot}->{logger}->log("Autojoining channels.\n");
2020-02-15 23:38:32 +01:00
$self->{pbot}->{channels}->autojoin;
}
2020-02-15 23:38:32 +01:00
return 0;
}
sub on_disconnect {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log("Disconnected...\n");
$self->{pbot}->{connected} = 0;
return 0;
}
sub on_motd {
2020-02-15 23:38:32 +01:00
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");
}
return 0;
}
2017-08-02 06:35:56 +02:00
sub on_self_join {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
return 0 if not $self->{pbot}->{registry}->get_value('general', 'send_who_on_join') // 1;
my $send_who = 0;
if ($self->{pbot}->{registry}->get_value('general', 'send_who_chanop_only') // 1) {
if ($self->{pbot}->{channels}->get_meta($event->{channel}, 'chanop')) {
$send_who = 1;
}
} else {
$send_who = 1;
}
2020-02-15 23:38:32 +01:00
$self->send_who($event->{channel}) if $send_who;
return 0;
2017-08-02 06:35:56 +02:00
}
sub on_self_part {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
return 0;
2017-08-02 06:35:56 +02:00
}
sub on_public {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
2020-02-15 23:38:32 +01:00
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];
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
$event->{interpreted} = $self->{pbot}->{interpreter}->process_line($from, $nick, $user, $host, $text);
return 0;
}
sub on_msg {
2020-02-15 23:38:32 +01:00
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');
$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 {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host);
my $text = $event->{event}->{args}[0];
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Received NOTICE from $nick!$user\@$host to $event->{event}->{to}[0] '$text'\n");
2020-02-15 23:38:32 +01:00
return 0 if not length $host;
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
if ($nick eq 'NickServ') {
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/) {
$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);
}
2020-02-15 23:38:32 +01:00
return 0;
}
sub on_action {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
2020-02-15 23:38:32 +01:00
$event->{event}->{args}[0] = "/me " . $event->{event}->{args}[0];
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
$self->on_public($event_type, $event);
return 0;
}
# FIXME: on_mode doesn't handle chanmodes that have parameters, e.g. +l
sub on_mode {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host);
2020-02-15 23:38:32 +01:00
my $mode_string = $event->{event}->{args}[0];
my $channel = $event->{event}->{to}[0];
$channel = lc $channel;
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
2020-02-15 23:38:32 +01:00
my ($mode, $mode_char, $modifier);
my $i = 0;
my $target;
2011-02-13 06:07:02 +01:00
2020-02-15 23:38:32 +01:00
while ($mode_string =~ m/(.)/g) {
my $char = $1;
2011-02-13 06:07:02 +01:00
2020-02-15 23:38:32 +01:00
if ($char eq '-' or $char eq '+') {
$modifier = $char;
next;
}
2011-02-13 06:07:02 +01:00
2020-02-15 23:38:32 +01:00
$mode = $modifier . $char;
$mode_char = $char;
$target = $event->{event}->{args}[++$i];
2011-02-13 06:07:02 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Mode $channel [$mode" . (length $target ? " $target" : '') . "] by $nick!$user\@$host\n");
2011-02-13 06:07:02 +01:00
$self->{pbot}->{banlist}->track_mode("$nick!$user\@$host", $channel, $mode, $target);
$self->{pbot}->{chanops}->track_mode("$nick!$user\@$host", $channel, $mode, $target);
2020-02-15 23:38:32 +01:00
if (defined $target and length $target) {
# mode set on user
2020-02-15 23:38:32 +01:00
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});
2020-02-15 23:38:32 +01:00
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
2020-02-15 23:38:32 +01:00
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;
}
$self->{pbot}->{channels}->{channels}->set($channel, 'MODE', $modes, 1);
}
}
}
2020-02-15 23:38:32 +01:00
return 0;
}
sub on_join {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to);
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
2020-02-15 23:38:32 +01:00
$channel = lc $channel;
2020-02-15 23:38:32 +01:00
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});
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->devalidate_channel($message_account, $channel);
2020-02-15 23:38:32 +01:00
my $msg = 'JOIN';
2020-02-15 23:38:32 +01:00
if (exists $self->{pbot}->{irc_capabilities}->{'extended-join'}) {
$msg .= " $event->{event}->{args}[0] :$event->{event}->{args}[1]";
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->update_gecos($message_account, $event->{event}->{args}[1], scalar gettimeofday);
2020-02-15 23:38:32 +01:00
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]);
} else {
$self->{pbot}->{messagehistory}->{database}->set_current_nickserv_account($message_account, '');
}
2020-02-15 23:38:32 +01:00
$self->{pbot}->{antiflood}->check_bans($message_account, $event->{event}->from, $channel);
}
2020-02-15 23:38:32 +01:00
$self->{pbot}->{antiflood}->check_flood(
$channel, $nick, $user, $host, $msg,
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_threshold'),
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_time_threshold'),
$self->{pbot}->{messagehistory}->{MSG_JOIN}
);
return 0;
}
sub on_invite {
2020-02-15 23:38:32 +01:00
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]);
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
2020-02-15 23:38:32 +01:00
$channel = lc $channel;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("$nick!$user\@$host invited $target to $channel!\n");
2020-02-15 23:38:32 +01:00
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); }
}
2020-02-15 23:38:32 +01:00
return 0;
}
2014-05-15 17:49:56 +02:00
sub on_kick {
2020-02-15 23:38:32 +01:00
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;
2014-05-15 17:49:56 +02:00
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("$nick!$user\@$host kicked $target from $channel ($reason)\n");
2014-05-15 17:49:56 +02:00
2020-02-15 23:38:32 +01:00
my ($message_account) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($target);
2014-05-15 17:49:56 +02:00
2020-02-15 23:38:32 +01:00
my $hostmask;
if (defined $message_account) {
$hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($message_account);
2014-05-15 17:49:56 +02:00
2020-02-15 23:38:32 +01:00
my ($target_nick, $target_user, $target_host) = $hostmask =~ m/^([^!]+)!([^@]+)@(.*)/;
my $text = "KICKED by $nick!$user\@$host ($reason)";
2014-05-15 17:49:56 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->add_message($message_account, $hostmask, $channel, $text, $self->{pbot}->{messagehistory}->{MSG_DEPARTURE});
$self->{pbot}->{antiflood}->check_flood(
$channel, $target_nick, $target_user, $target_host, $text,
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_threshold'),
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_time_threshold'),
$self->{pbot}->{messagehistory}->{MSG_DEPARTURE}
);
}
2020-02-15 23:38:32 +01:00
$message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account_id("$nick!$user\@$host");
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
if (defined $message_account) {
my $text = "KICKED " . (defined $hostmask ? $hostmask : $target) . " from $channel ($reason)";
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, $text, $self->{pbot}->{messagehistory}->{MSG_CHAT});
}
return 0;
2014-05-15 17:49:56 +02:00
}
sub on_departure {
2020-02-15 23:38:32 +01:00
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;
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
2020-02-15 23:38:32 +01:00
my $text = uc $event->{event}->type;
$text .= " $args";
2020-02-15 23:38:32 +01:00
my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host);
2020-02-15 23:38:32 +01:00
if ($text =~ m/^QUIT/) {
# QUIT messages must be dispatched to each channel the user is on
my $channels = $self->{pbot}->{nicklist}->get_channels($nick);
foreach my $chan (@$channels) {
next if $chan !~ m/^#/;
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $chan, $text, $self->{pbot}->{messagehistory}->{MSG_DEPARTURE});
}
} else {
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, $text, $self->{pbot}->{messagehistory}->{MSG_DEPARTURE});
}
$self->{pbot}->{antiflood}->check_flood(
$channel, $nick, $user, $host, $text,
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_threshold'),
$self->{pbot}->{registry}->get_value('antiflood', 'join_flood_time_threshold'),
$self->{pbot}->{messagehistory}->{MSG_DEPARTURE}
);
my $u = $self->{pbot}->{users}->find_user($channel, "$nick!$user\@$host");
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;
}
2020-02-15 23:38:32 +01:00
return 0;
}
sub on_map {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
# remove and discard first and last elements
shift @{$event->{event}->{args}};
pop @{$event->{event}->{args}};
foreach my $arg (@{$event->{event}->{args}}) {
my ($key, $value) = split /=/, $arg;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{ircd}->{$key} = $value;
if (not defined $value) {
$self->{pbot}->{logger}->log(" $key\n");
} else {
$self->{pbot}->{logger}->log(" $key=$value\n");
}
2020-02-15 23:38:32 +01:00
}
}
# IRCv3 client capability negotiation
# TODO: most, if not all, of this should probably be in PBot::IRC::Connection
# but at the moment I don't want to change Net::IRC more than the absolute
# minimum necessary.
#
# TODO: CAP NEW and CAP DEL
sub on_cap {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
# configure client capabilities that PBot currently supports
my %desired_caps = (
'account-notify' => 1,
'extended-join' => 1,
# TODO: unsupported capabilities worth looking into
'away-notify' => 0,
'chghost' => 0,
'identify-msg' => 0,
'multi-prefix' => 0,
);
if ($event->{event}->{args}->[0] eq 'LS') {
my $capabilities;
my $caps_done = 0;
if ($event->{event}->{args}->[1] eq '*') {
# more CAP LS messages coming
$capabilities = $event->{event}->{args}->[2];
} else {
# final CAP LS message
$caps_done = 1;
$capabilities = $event->{event}->{args}->[1];
}
$self->{pbot}->{logger}->log("Client capabilities available: $capabilities\n");
my @caps = split /\s+/, $capabilities;
foreach my $cap (@caps) {
my $value;
if ($cap =~ /=/) {
($cap, $value) = split /=/, $cap;
} else {
$value = 1;
}
# store available capability
$self->{pbot}->{irc_capabilities_available}->{$cap} = $value;
# request desired capabilities
if ($desired_caps{$cap}) {
$self->{pbot}->{logger}->log("Requesting client capability $cap\n");
$event->{conn}->sl("CAP REQ :$cap");
}
}
# capability negotiation done
# now we either start SASL authentication or we send CAP END
if ($caps_done) {
# start SASL authentication if enabled
if ($self->{pbot}->{registry}->get_value('irc', 'sasl')) {
$self->{pbot}->{logger}->log("Requesting client capability sasl\n");
$event->{conn}->sl("CAP REQ :sasl");
} else {
$self->{pbot}->{logger}->log("Completed client capability negotiation\n");
$event->{conn}->sl("CAP END");
}
}
}
elsif ($event->{event}->{args}->[0] eq 'ACK') {
$self->{pbot}->{logger}->log("Client capabilities granted: $event->{event}->{args}->[1]\n");
2020-02-15 23:38:32 +01:00
my @caps = split /\s+/, $event->{event}->{args}->[1];
foreach my $cap (@caps) {
$self->{pbot}->{irc_capabilities}->{$cap} = 1;
if ($cap eq 'sasl') {
# begin SASL authentication
# TODO: for now we support only PLAIN
$self->{pbot}->{logger}->log("Performing SASL authentication [PLAIN]\n");
$event->{conn}->sl("AUTHENTICATE PLAIN");
}
}
}
elsif ($event->{event}->{args}->[0] eq 'NAK') {
$self->{pbot}->{logger}->log("Client capabilities rejected: $event->{event}->{args}->[1]\n");
}
else {
$self->{pbot}->{logger}->log("Unknown CAP event:\n");
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log(Dumper $event->{event});
}
return 0;
}
# IRCv3 SASL authentication
# TODO: this should probably be in PBot::IRC::Connection as well...
# but at the moment I don't want to change Net::IRC more than the absolute
# minimum necessary.
sub on_sasl_authenticate {
my ($self, $event_type, $event) = @_;
my $nick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
my $password = $self->{pbot}->{registry}->get_value('irc', 'identify_password');
if (not defined $password or not length $password) {
$self->{pbot}->{logger}->log("Error: Registry entry irc.identify_password is not set.\n");
$self->{pbot}->exit;
}
$password = encode('UTF-8', "$nick\0$nick\0$password");
$password = encode_base64($password, '');
my @chunks = unpack('(A400)*', $password);
foreach my $chunk (@chunks) {
$event->{conn}->sl("AUTHENTICATE $chunk");
}
# must send final AUTHENTICATE + if last chunk was exactly 400 bytes
if (length $chunks[$#chunks] == 400) {
$event->{conn}->sl("AUTHENTICATE +");
}
return 0;
}
sub on_rpl_loggedin {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
return 0;
}
sub on_rpl_loggedout {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
return 0;
}
sub on_err_nicklocked {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$self->{pbot}->exit;
}
sub on_rpl_saslsuccess {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$event->{conn}->sl("CAP END");
return 0;
}
sub on_err_saslfail {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$self->{pbot}->exit;
}
sub on_err_sasltoolong {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$self->{pbot}->exit;
}
sub on_err_saslaborted {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$self->{pbot}->exit;
}
sub on_err_saslalready {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
2020-02-15 23:38:32 +01:00
return 0;
}
sub on_rpl_saslmechs {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log("SASL mechanism not available.\n");
$self->{pbot}->{logger}->log("Available mechanisms are: $event->{event}->{args}->[1]\n");
$self->{pbot}->exit;
}
sub on_nickchange {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $newnick) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->args);
($nick, $user, $host) = $self->normalize_hostmask($nick, $user, $host);
$self->{pbot}->{logger}->log("[NICKCHANGE] $nick!$user\@$host changed nick to $newnick\n");
if ($newnick eq $self->{pbot}->{registry}->get_value('irc', 'botnick') and not $self->{pbot}->{joined_channels}) {
$self->{pbot}->{channels}->autojoin;
return 0;
}
2020-02-15 23:38:32 +01:00
my $message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host);
$self->{pbot}->{messagehistory}->{database}->devalidate_all_channels($message_account, $self->{pbot}->{antiflood}->{NEEDS_CHECKBAN});
my $channels = $self->{pbot}->{nicklist}->get_channels($nick);
foreach my $channel (@$channels) {
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});
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});
2020-02-15 23:38:32 +01:00
$self->{pbot}->{antiflood}->check_flood(
"$nick!$user\@$host", $nick, $user, $host, "NICKCHANGE $newnick",
$self->{pbot}->{registry}->get_value('antiflood', 'nick_flood_threshold'),
$self->{pbot}->{registry}->get_value('antiflood', 'nick_flood_time_threshold'),
$self->{pbot}->{messagehistory}->{MSG_NICKCHANGE}
);
return 0;
}
sub on_nicknameinuse {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my (undef, $nick, $msg) = $event->{event}->args;
my $from = $event->{event}->from;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Received nicknameinuse for nick $nick from $from: $msg\n");
$event->{conn}->privmsg("nickserv", "ghost $nick " . $self->{pbot}->{registry}->get_value('irc', 'identify_password'));
return 0;
}
sub on_channelmodeis {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
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 {
2020-02-15 23:38:32 +01:00
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);
}
sub on_topic {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
if (not length $event->{event}->{to}->[0]) {
# on join
my (undef, $channel, $topic) = $event->{event}->args;
$self->{pbot}->{logger}->log("Topic for $channel: $topic\n");
$self->{pbot}->{channels}->{channels}->set($channel, 'TOPIC', $topic, 1);
} else {
# user changing topic
my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host);
my $channel = $event->{event}->{to}->[0];
my $topic = $event->{event}->{args}->[0];
$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);
}
}
sub on_topicinfo {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my (undef, $channel, $by, $timestamp) = $event->{event}->args;
$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);
}
sub normalize_hostmask {
2020-02-15 23:38:32 +01:00
my ($self, $nick, $user, $host) = @_;
2020-02-15 23:38:32 +01:00
if ($host =~ m{^(gateway|nat)/(.*)/x-[^/]+$}) { $host = "$1/$2/x-$user"; }
2020-02-15 23:38:32 +01:00
$host =~ s{/session$}{/x-$user};
2017-06-20 03:21:47 +02:00
2020-02-15 23:38:32 +01:00
return ($nick, $user, $host);
}
2017-08-02 06:35:56 +02:00
my %who_queue;
my %who_cache;
my $last_who_id;
my $who_pending = 0;
2019-12-31 01:44:41 +01:00
sub on_whoreply {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
my (undef, $id, $user, $host, $server, $nick, $usermodes, $gecos) = @{$event->{event}->{args}};
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host);
my $hostmask = "$nick!$user\@$host";
my $channel;
if ($id =~ m/^#/) {
$id = lc $id;
foreach my $x (keys %who_cache) {
if ($who_cache{$x} eq $id) {
$id = $x;
last;
}
}
2019-12-31 01:44:41 +01:00
}
2020-02-15 23:38:32 +01:00
$last_who_id = $id;
$channel = $who_cache{$id};
delete $who_queue{$id};
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
return 0 if not defined $channel;
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("WHO id: $id [$channel], hostmask: $hostmask, $usermodes, $server, $gecos.\n");
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{nicklist}->add_nick($channel, $nick);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'hostmask', $hostmask);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'user', $user);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'host', $host);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'server', $server);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'gecos', $gecos);
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
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});
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->link_aliases($account_id, $hostmask, undef);
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->devalidate_channel($account_id, $channel);
$self->{pbot}->{antiflood}->check_bans($account_id, $hostmask, $channel);
2019-12-31 01:44:41 +01:00
2020-02-15 23:38:32 +01:00
return 0;
2019-12-31 01:44:41 +01:00
}
2017-08-02 06:35:56 +02:00
sub on_whospcrpl {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
2017-08-02 06:35:56 +02:00
my (undef, $id, $user, $host, $nick, $nickserv, $gecos) = @{$event->{event}->{args}};
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host);
$last_who_id = $id;
my $hostmask = "$nick!$user\@$host";
my $channel = $who_cache{$id};
delete $who_queue{$id};
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
return 0 if not defined $channel;
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("WHO id: $id [$channel], hostmask: $hostmask, $nickserv, $gecos.\n");
2020-02-15 23:38:32 +01:00
$self->{pbot}->{nicklist}->add_nick($channel, $nick);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'hostmask', $hostmask);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'user', $user);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'host', $host);
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'nickserv', $nickserv) if $nickserv ne '0';
$self->{pbot}->{nicklist}->set_meta($channel, $nick, 'gecos', $gecos);
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
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});
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
if ($nickserv ne '0') {
$self->{pbot}->{messagehistory}->{database}->link_aliases($account_id, undef, $nickserv);
$self->{pbot}->{antiflood}->check_nickserv_accounts($nick, $nickserv);
}
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->link_aliases($account_id, $hostmask, undef);
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{messagehistory}->{database}->devalidate_channel($account_id, $channel);
$self->{pbot}->{antiflood}->check_bans($account_id, $hostmask, $channel);
2017-08-02 06:35:56 +02:00
2020-02-15 23:38:32 +01:00
return 0;
2017-08-02 06:35:56 +02:00
}
sub on_endofwho {
2020-02-15 23:38:32 +01:00
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log("WHO session $last_who_id ($who_cache{$last_who_id}) completed.\n");
delete $who_cache{$last_who_id};
delete $who_queue{$last_who_id};
$who_pending = 0;
return 0;
2017-08-02 06:35:56 +02:00
}
sub send_who {
2020-02-15 23:38:32 +01:00
my ($self, $channel) = @_;
$channel = lc $channel;
$self->{pbot}->{logger}->log("pending WHO to $channel\n");
for (my $id = 1; $id < 99; $id++) {
if (not exists $who_cache{$id}) {
$who_cache{$id} = $channel;
$who_queue{$id} = $channel;
$last_who_id = $id;
last;
}
2017-08-02 06:35:56 +02:00
}
}
sub check_pending_whos {
2020-02-15 23:38:32 +01:00
my $self = shift;
return if $who_pending;
foreach my $id (keys %who_queue) {
$self->{pbot}->{logger}->log("sending WHO to $who_queue{$id} [$id]\n");
$self->{pbot}->{conn}->sl("WHO $who_queue{$id} %tuhnar,$id");
$who_pending = 1;
$last_who_id = $id;
last;
}
2017-08-02 06:35:56 +02:00
}
1;