Support IRCv3 message tags; misc refactoring

PBot now finally supports IRCv3 message tags. The `account-tag`
capability is now enabled for improved NickServ account tracking.

Refactored CAP negotiation. Minor clean-ups in AntiFlood.pm (which ought to
be completely rewritten from the ground-up).

Misc clean-ups and refactoring of various things.
This commit is contained in:
Pragmatic Software 2023-01-27 11:48:01 -08:00
parent 19d8170af3
commit 999c07f237
9 changed files with 161 additions and 81 deletions

View File

@ -25,8 +25,10 @@ sub initialize {
my ($self, %conf) = @_;
# flags for 'validated' field
$self->{NICKSERV_VALIDATED} = (1 << 0);
$self->{NEEDS_CHECKBAN} = (1 << 1);
use constant {
NICKSERV_VALIDATED => (1 << 0),
NEEDS_CHECKBAN => (1 << 1),
};
$self->{channels} = {}; # per-channel statistics, e.g. for optimized tracking of last spoken nick for enter-abuse detection, etc
$self->{nickflood} = {}; # statistics to track nickchange flooding
@ -188,6 +190,29 @@ sub check_flood {
return;
}
my $needs_checkban = 0;
if (defined $context && defined $context->{tags}) {
my $tags = $self->{pbot}->{irc}->get_tags($context->{tags});
if (defined $tags->{account}) {
my $nickserv_account = $tags->{account};
my $current_nickserv_account = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($account);
if ($self->{pbot}->{registry}->get_value('irc', 'debug_tags')) {
$self->{pbot}->{logger}->log("($account) $mask got account-tag $nickserv_account\n");
}
if ($current_nickserv_account ne $nickserv_account) {
$self->{pbot}->{logger}->log("[MH] ($account) $mask updating NickServ to $nickserv_account\n");
$self->{pbot}->{messagehistory}->{database}->set_current_nickserv_account($account, $nickserv_account);
$self->{pbot}->{messagehistory}->{database}->update_nickserv_account($account, $nickserv_account, scalar gettimeofday);
$self->{pbot}->{messagehistory}->{database}->link_aliases($account, $mask, $nickserv_account);
$needs_checkban = 1;
}
}
}
my $channels;
if ($mode == MSG_NICKCHANGE) {
$channels = $self->{pbot}->{nicklist}->get_channels($oldnick);
@ -205,14 +230,16 @@ sub check_flood {
if ($chan =~ /^#/ and $mode == MSG_DEPARTURE) {
# remove validation on PART or KICK so we check for ban-evasion when user returns at a later time
my $chan_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $chan, 'validated');
if ($chan_data->{validated} & $self->{NICKSERV_VALIDATED}) {
$chan_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
if ($chan_data->{validated} & NICKSERV_VALIDATED) {
$chan_data->{validated} &= ~NICKSERV_VALIDATED;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $chan, $chan_data);
}
next;
}
if ($self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted')) { next; }
next if $self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted');
$self->check_bans($account, $mask, $chan) if $needs_checkban;
if ($max_messages > $self->{pbot}->{registry}->get_value('messagehistory', 'max_messages')) {
$self->{pbot}->{logger}->log("Warning: max_messages greater than max_messages limit; truncating.\n");
@ -223,7 +250,7 @@ sub check_flood {
if ($chan =~ m/^#/) {
my $validated = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $chan, 'validated')->{'validated'};
if ($validated & $self->{NEEDS_CHECKBAN} or not $validated & $self->{NICKSERV_VALIDATED}) {
if ($validated & NEEDS_CHECKBAN or not $validated & NICKSERV_VALIDATED) {
if ($mode == MSG_DEPARTURE) {
# don't check for evasion on PART/KICK
} elsif ($mode == MSG_NICKCHANGE) {
@ -575,8 +602,8 @@ sub devalidate_accounts {
foreach my $account (@message_accounts) {
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'validated');
if (defined $channel_data and $channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
if (defined $channel_data and $channel_data->{validated} & NICKSERV_VALIDATED) {
$channel_data->{validated} &= ~NICKSERV_VALIDATED;
#$self->{pbot}->{logger}->log("Devalidating account $account\n");
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
@ -605,16 +632,16 @@ sub check_bans {
if (defined $current_nickserv_account and length $current_nickserv_account) {
$self->{pbot}->{logger}->log("anti-flood: [check-bans] current nickserv [$current_nickserv_account] found for $mask\n") if $debug_checkban >= 2;
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
if ($channel_data->{validated} & $self->{NEEDS_CHECKBAN}) {
$channel_data->{validated} &= ~$self->{NEEDS_CHECKBAN};
if ($channel_data->{validated} & NEEDS_CHECKBAN) {
$channel_data->{validated} &= ~NEEDS_CHECKBAN;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
}
} else {
if (not exists $self->{pbot}->{irc_capabilities}->{'account-notify'}) {
# mark this account as needing check-bans when nickserv account is identified
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
if (not $channel_data->{validated} & $self->{NEEDS_CHECKBAN}) {
$channel_data->{validated} |= $self->{NEEDS_CHECKBAN};
if (not $channel_data->{validated} & NEEDS_CHECKBAN) {
$channel_data->{validated} |= NEEDS_CHECKBAN;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
}
$self->{pbot}->{logger}->log("anti-flood: [check-bans] no account for $mask; marking for later validation\n") if $debug_checkban >= 1;
@ -685,8 +712,8 @@ sub check_bans {
$self->{pbot}->{logger}
->log("anti-flood: [check-bans] $mask [$alias] evaded $baninfo->{mask} in $baninfo->{channel}, but within 5 seconds of establishing ban; giving another chance\n");
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
if ($channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
if ($channel_data->{validated} & NICKSERV_VALIDATED) {
$channel_data->{validated} &= ~NICKSERV_VALIDATED;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
}
$do_not_validate = 1;
@ -792,8 +819,8 @@ sub check_bans {
);
}
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
if ($channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
if ($channel_data->{validated} & NICKSERV_VALIDATED) {
$channel_data->{validated} &= ~NICKSERV_VALIDATED;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
}
return;
@ -802,8 +829,8 @@ sub check_bans {
unless ($do_not_validate) {
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
if (not $channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
$channel_data->{validated} |= $self->{NICKSERV_VALIDATED};
if (not $channel_data->{validated} & NICKSERV_VALIDATED) {
$channel_data->{validated} |= NICKSERV_VALIDATED;
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
}
}
@ -825,7 +852,7 @@ sub on_endofwhois {
foreach my $channel (@$channels) {
next unless $channel =~ /^#/;
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($id, $channel, 'validated');
if ($channel_data->{validated} & $self->{NEEDS_CHECKBAN} or not $channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
if ($channel_data->{validated} & NEEDS_CHECKBAN or not $channel_data->{validated} & NICKSERV_VALIDATED) {
$self->check_bans($id, $hostmask, $channel);
}
}
@ -870,7 +897,7 @@ sub on_accountnotify {
my $mask = $event->{event}->{from};
my ($nick, $user, $host) = $mask =~ m/^([^!]+)!([^@]+)@(.*)/;
my $account = $event->{event}->{args}[0];
my $account = $event->{event}{args}[0];
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host);
$self->{pbot}->{messagehistory}->{database}->update_hostmask_data($mask, {last_seen => scalar gettimeofday});

View File

@ -10,6 +10,8 @@ package PBot::Core::Handlers::Cap;
use PBot::Imports;
use parent 'PBot::Core::Class';
use POSIX qw/EXIT_FAILURE/;
sub initialize {
my ($self, %conf) = @_;
@ -22,28 +24,16 @@ sub initialize {
sub on_cap {
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;
my $caps_listed = 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;
$caps_listed = 1;
$capabilities = $event->{event}->{args}->[1];
}
@ -51,36 +41,20 @@ sub on_cap {
my @caps = split /\s+/, $capabilities;
# store available capabilities
foreach my $cap (@caps) {
my $value;
if ($cap =~ /=/) {
($cap, $value) = split /=/, $cap;
} else {
$value = 1;
}
($cap, $value) = split /=/, $cap;
$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");
}
# all capabilities listed?
if ($caps_listed) {
# request desired capabilities
$self->request_caps($event);
}
}
elsif ($event->{event}->{args}->[0] eq 'ACK') {
@ -89,7 +63,10 @@ sub on_cap {
my @caps = split /\s+/, $event->{event}->{args}->[1];
foreach my $cap (@caps) {
$self->{pbot}->{irc_capabilities}->{$cap} = 1;
my ($key, $val) = split '=', $cap;
$val //= 1;
$self->{pbot}->{irc_capabilities}->{$key} = $val;
if ($cap eq 'sasl') {
# begin SASL authentication
@ -111,4 +88,45 @@ sub on_cap {
return 1;
}
sub request_caps {
my ($self, $event) = @_;
# configure client capabilities that PBot currently supports
my %desired_caps = (
'account-notify' => 1,
'account-tag' => 1,
'extended-join' => 1,
'message-tags' => 1,
# sasl is gated by the irc.sasl registry entry instead
# TODO: unsupported capabilities worth looking into
'away-notify' => 0,
'chghost' => 0,
'identify-msg' => 0,
'multi-prefix' => 0,
);
foreach my $cap (keys $self->{pbot}->{irc_capabilities_available}->%*) {
# request desired capabilities
if ($desired_caps{$cap}) {
$self->{pbot}->{logger}->log("Requesting client capability $cap\n");
$event->{conn}->sl("CAP REQ :$cap");
}
}
# request SASL capability if enabled, otherwise end cap negotiation
if ($self->{pbot}->{registry}->get_value('irc', 'sasl')) {
if (not exists $self->{pbot}->{irc_capabilities_available}->{sasl}) {
$self->{pbot}->{logger}->log("SASL is not supported by this IRC server\n");
$self->{pbot}->exit(EXIT_FAILURE);
}
$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");
}
}
1;

View File

@ -52,18 +52,19 @@ sub on_notice {
sub on_public {
my ($self, $event_type, $event) = @_;
my ($from, $nick, $user, $host, $text) = (
my ($from, $nick, $user, $host, $text, $tags) = (
$event->{event}->{to}->[0],
$event->{event}->nick,
$event->{event}->user,
$event->{event}->host,
$event->{event}->{args}->[0],
$event->{event}->{args}->[1],
);
($nick, $user, $host) = $self->{pbot}->{irchandlers}->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);
$event->{interpreted} = $self->{pbot}->{interpreter}->process_line($from, $nick, $user, $host, $text, $tags);
return 1;
}
@ -82,17 +83,18 @@ sub on_action {
sub on_msg {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $text) = (
my ($nick, $user, $host, $text, $tags) = (
$event->{event}->nick,
$event->{event}->user,
$event->{event}->host,
$event->{event}->{args}->[0],
$event->{event}->{args}->[1],
);
($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host);
# send text to be processed as a bot command, in "channel" $nick
$event->{interpreted} = $self->{pbot}->{interpreter}->process_line($nick, $nick, $user, $host, $text, 1);
$event->{interpreted} = $self->{pbot}->{interpreter}->process_line($nick, $nick, $user, $host, $text, $tags, 1);
return 1;
}

View File

@ -10,7 +10,7 @@ package PBot::Core::Handlers::SASL;
use PBot::Imports;
use parent 'PBot::Core::Class';
use POSIX qw/EXIT_SUCCESS EXIT_FAILURE/;
use POSIX qw/EXIT_FAILURE/;
use Encode;
use MIME::Base64;
@ -60,7 +60,7 @@ sub on_sasl_authenticate {
sub on_rpl_loggedin {
my ($self, $event_type, $event) = @_;
$self->{pbot}->{logger}->log($event->{event}->{args}->[1] . "\n");
$self->{pbot}->{logger}->log($event->{event}->{args}->[3] . "\n");
return 1;
}

View File

@ -260,6 +260,23 @@ sub timeout {
return $self->{_timeout};
}
# parses message tags into a hashref
sub get_tags {
my ($self, $tags) = @_;
$self->{_pbot}->{logger}->log("Message tags: [$tags]\n");
my @list = split ';', $tags;
my %hash;
foreach my $tag (@list) {
my ($key, $val) = split '=', $tag;
$hash{$key} = $val;
}
return \%hash;
}
1;
__END__

View File

@ -508,9 +508,9 @@ sub handler {
croak "Not enough arguments to handler()";
}
print STDERR "Trying to handle event '$ev'.\n" if $self->{_debug};
print STDERR "--- Trying to handle event '$ev'.\n" if $self->{_debug};
if ($self->{_debug}) {
if ($self->{_debug} > 1) {
use Data::Dumper;
print STDERR "ev: ", Dumper($ev), "\nevent: ", Dumper($event), "\n";
}
@ -535,7 +535,7 @@ sub handler {
confess "Bad parameter passed to handler(): rp=$rp";
}
print STDERR "Handler for '$ev' called.\n" if $self->{_debug};
print STDERR "--- Handler for '$ev' called.\n" if $self->{_debug};
return 1;
}
@ -838,7 +838,7 @@ sub oper {
# appropriate handler. Takes no args, really.
sub parse {
my ($self) = shift;
my ($from, $type, $message, @stuff, $itype, $ev, @lines, $line);
my ($from, $type, $message, @stuff, $itype, $ev, @lines, $line, $tags);
my $n;
@ -920,7 +920,9 @@ sub parse {
# Spurious backslashes are for the benefit of cperl-mode.
# Assumption: all non-numeric message types begin with a letter
} elsif (
$line =~ /^:?
$line =~ /^
(?:\@\S+\s)? # Optional message tags
:? # Initial colon
(?:[][}{\w\\\`^|\-]+? # The nick (valid nickname chars)
! # The nick-username separator
.+? # The username
@ -932,6 +934,8 @@ sub parse {
/x
) # That ought to do it for now...
{
$tags = undef;
$tags = $1 if $line =~ s/^@(\S+)\s//;
$line = substr $line, 1 if $line =~ /^:/;
# Patch submitted for v.0.72
@ -939,7 +943,7 @@ sub parse {
# ($from, $line) = split ":", $line, 2;
($from, $line) = $line =~ /^(?:|)(\S+\s+[^:]+):?(.*)/;
print STDERR "from: [$from], line: [$line]\n" if $self->{_debug};
print STDERR "from: [$from], line: [$line]\n" if $self->{_debug} > 2;
($from, $type, @stuff) = split /\s+/, $from;
$type = lc $type;
@ -986,8 +990,9 @@ sub parse {
# quotemeta($_))); /$from/ }
# ($self->ignore($type), $self->ignore("all"));
# Add $line to @stuff for the handlers
# Add $line and $tags to @stuff for the handlers
push @stuff, $line if defined $line;
push @stuff, $tags if defined $tags;
# Now ship it off to the appropriate handler and forget about it.
if ($itype eq "ctcp") { # it's got CTCP in it!
@ -1251,6 +1256,9 @@ sub privmsg {
my $buf = CORE::join '', @_;
my $length = $self->{_maxlinelen} - 11 - length($to);
print STDERR "privmsg trunc length: $length; msg len: " . (length $buf) . "\n" if $self->{_debug};
my $line;
if (ref($to) =~ /^(GLOB|IO::Socket)/) {
@ -1261,8 +1269,11 @@ sub privmsg {
} else {
while (length($buf) > 0) {
($line, $buf) = unpack("a$length a*", $buf);
if (ref $to eq 'ARRAY') { $self->sl("PRIVMSG ", CORE::join(',', @$to), " :$line"); }
else { $self->sl("PRIVMSG $to :$line"); }
if (ref $to eq 'ARRAY') {
$self->sl("PRIVMSG ", CORE::join(',', @$to), " :$line");
} else {
$self->sl("PRIVMSG $to :$line");
}
}
}
}
@ -1319,7 +1330,7 @@ sub schedule {
unless ($coderef) { croak 'Not enough arguments to Connection->schedule()'; }
unless (ref($coderef) eq 'CODE') { croak 'Second argument to schedule() isn\'t a coderef'; }
print STDERR "Scheduling event with time [$time]\n" if $self->{_debug};
print STDERR "Scheduling event with time [$time]\n" if $self->{_debug} > 1;
$time += time;
$self->parent->enqueue_scheduled_event($time, $coderef, $self, @_);
}
@ -1332,7 +1343,7 @@ sub schedule_output_event {
unless ($coderef) { croak 'Not enough arguments to Connection->schedule()'; }
unless (ref($coderef) eq 'CODE') { croak 'Second argument to schedule() isn\'t a coderef'; }
print STDERR "Scheduling output event with time [$time] [$_[0]]\n" if $self->{_debug};
print STDERR "Scheduling output event with time [$time] [$_[0]]\n" if $self->{_debug} > 1;
$time += time;
$self->parent->enqueue_output_event($time, $coderef, $self, @_);
}
@ -1395,6 +1406,10 @@ sub sl {
if ($self->{_slcount} < 10) {
$self->{_slcount}++;
$self->{_lastsl} = time;
### DEBUG DEBUG DEBUG
if ($self->{_debug} > 1) { print STDERR "S-> 0 " . (length ($line) + 2) . " $line\n"; }
return $self->schedule_output_event(0, \&sl_real, $line);
}
@ -1413,7 +1428,7 @@ sub sl {
}
### DEBUG DEBUG DEBUG
if ($self->{_debug}) { print STDERR "S-> $seconds $line\n"; }
if ($self->{_debug} > 1) { print STDERR "S-> $seconds " . (length ($line) + 2) . " $line\n"; }
$self->schedule_output_event($seconds, \&sl_real, $line);
}
@ -1428,7 +1443,7 @@ sub sl_real {
unless ($line) { croak "Not enough arguments to sl_real()"; }
### DEBUG DEBUG DEBUG
if ($self->{_debug}) { print STDERR ">>> $line\n"; }
if ($self->{_debug}) { print STDERR ">>> (" . (length ($line) + 2) . ") $line\n"; }
return unless defined $self->socket;

View File

@ -38,7 +38,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, $is_command) = @_;
my ($self, $from, $nick, $user, $host, $text, $tags, $is_command) = @_;
# lowercase `from` field for case-insensitivity
$from = lc $from;
@ -57,6 +57,7 @@ sub process_line {
host => $host, # hostname/ip address
hostmask => "$nick!$user\@$host", # full hostmask
text => $text, # message contents
tags => $tags, # message tags
};
# add hostmask to user/message tracking database and get their account id

View File

@ -68,7 +68,7 @@ sub stdin_reader {
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
# process input as a bot command
return $self->{pbot}->{interpreter}->process_line('stdin@pbot', $botnick, "stdin", "pbot", $input, 1);
return $self->{pbot}->{interpreter}->process_line('stdin@pbot', $botnick, "stdin", "pbot", $input, undef, 1);
}
1;

View File

@ -25,8 +25,8 @@ use PBot::Imports;
# These are set by the /misc/update_version script
use constant {
BUILD_NAME => "PBot",
BUILD_REVISION => 4605,
BUILD_DATE => "2023-01-24",
BUILD_REVISION => 4606,
BUILD_DATE => "2023-01-27",
};
sub initialize {}