diff --git a/lib/PBot/Core/AntiFlood.pm b/lib/PBot/Core/AntiFlood.pm index 895e4b14..830a080a 100644 --- a/lib/PBot/Core/AntiFlood.pm +++ b/lib/PBot/Core/AntiFlood.pm @@ -33,10 +33,6 @@ sub initialize { $self->{whois_pending} = {}; # prevents multiple whois for nick joining multiple channels at once $self->{changinghost} = {}; # tracks nicks changing hosts/identifying to strongly link them - my $filename = $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/ban-exemptions'; - $self->{'ban-exemptions'} = PBot::Storage::DualIndexHashObject->new(name => 'Ban exemptions', filename => $filename, pbot => $self->{pbot}); - $self->{'ban-exemptions'}->load; - $self->{pbot}->{event_queue}->enqueue(sub { $self->adjust_offenses }, 60 * 60 * 1, 'Adjust anti-flood offenses'); $self->{pbot}->{registry}->add_default('text', 'antiflood', 'enforce', $conf{enforce_antiflood} // 1); @@ -60,178 +56,12 @@ sub initialize { $self->{pbot}->{registry}->add_default('text', 'antiflood', 'debug_checkban', $conf{debug_checkban} // 0); - $self->{pbot}->{commands}->register(sub { $self->cmd_unbanme(@_) }, "unbanme", 0); - $self->{pbot}->{commands}->register(sub { $self->cmd_ban_exempt(@_) }, "ban-exempt", 1); - $self->{pbot}->{capabilities}->add('admin', 'can-ban-exempt', 1); - $self->{pbot}->{event_dispatcher}->register_handler('irc.whoisaccount', sub { $self->on_whoisaccount(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.whoisuser', sub { $self->on_whoisuser(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.endofwhois', sub { $self->on_endofwhois(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.account', sub { $self->on_accountnotify(@_) }); } -sub cmd_ban_exempt { - my ($self, $context) = @_; - my $arglist = $context->{arglist}; - $self->{pbot}->{interpreter}->lc_args($arglist); - - my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); - return "Usage: ban-exempt , where commands are: list, add, remove" if not defined $command; - - given ($command) { - when ($_ eq 'list') { - my $text = "Ban-evasion exemptions:\n"; - my $entries = 0; - foreach my $channel ($self->{'ban-exemptions'}->get_keys) { - $text .= ' ' . $self->{'ban-exemptions'}->get_key_name($channel) . ":\n"; - foreach my $mask ($self->{'ban-exemptions'}->get_keys($channel)) { - $text .= " $mask,\n"; - $entries++; - } - } - $text .= "none" if $entries == 0; - return $text; - } - when ("add") { - my ($channel, $mask) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: ban-exempt add " if not defined $channel or not defined $mask; - - my $data = { - owner => $context->{hostmask}, - created_on => scalar gettimeofday - }; - - $self->{'ban-exemptions'}->add($channel, $mask, $data); - return "/say $mask exempted from ban-evasions in channel $channel"; - } - when ("remove") { - my ($channel, $mask) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: ban-exempt remove " if not defined $channel or not defined $mask; - return $self->{'ban-exemptions'}->remove($channel, $mask); - } - default { return "Unknown command '$command'; commands are: list, add, remove"; } - } -} - -sub cmd_unbanme { - my ($self, $context) = @_; - my $unbanned; - - my %aliases = $self->{pbot}->{messagehistory}->{database}->get_also_known_as($context->{nick}); - - foreach my $alias (keys %aliases) { - next if $aliases{$alias}->{type} == $self->{pbot}->{messagehistory}->{database}->{alias_type}->{WEAK}; - next if $aliases{$alias}->{nickchange} == 1; - - my $join_flood_channel = $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_channel') // '#stop-join-flood'; - - my ($anick, $auser, $ahost) = $alias =~ m/([^!]+)!([^@]+)@(.*)/; - my $banmask = $self->address_to_mask($ahost); - my $mask = "*!$auser\@$banmask\$$join_flood_channel"; - - my @channels = $self->{pbot}->{messagehistory}->{database}->get_channels($aliases{$alias}->{id}); - - foreach my $channel (@channels) { - next if exists $unbanned->{$channel} and exists $unbanned->{$channel}->{$mask}; - next if not $self->{pbot}->{banlist}->{banlist}->exists($channel . '-floodbans', $mask); - - my $message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account($anick, $auser, $ahost); - my @nickserv_accounts = $self->{pbot}->{messagehistory}->{database}->get_nickserv_accounts($message_account); - - push @nickserv_accounts, undef; - - foreach my $nickserv_account (@nickserv_accounts) { - my $baninfos = $self->{pbot}->{banlist}->get_baninfo($channel, "$anick!$auser\@$ahost", $nickserv_account); - - if (defined $baninfos) { - foreach my $baninfo (@$baninfos) { - my $u = $self->{pbot}->{users}->loggedin($baninfo->{channel}, $context->{hostmask}); - my $whitelisted = $self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted'); - if ($self->ban_exempted($baninfo->{channel}, $baninfo->{mask}) || $whitelisted) { - $self->{pbot}->{logger}->log("anti-flood: [unbanme] $anick!$auser\@$ahost banned as $baninfo->{mask} in $baninfo->{channel}, but allowed through whitelist\n"); - } else { - if ($channel eq lc $baninfo->{channel}) { - my $mode = $baninfo->{type} eq 'b' ? "banned" : "quieted"; - $self->{pbot}->{logger}->log("anti-flood: [unbanme] $anick!$auser\@$ahost $mode as $baninfo->{mask} in $baninfo->{channel} by $baninfo->{owner}, unbanme rejected\n"); - return "/msg $context->{nick} You have been $mode as $baninfo->{mask} by $baninfo->{owner}, unbanme will not work until it is removed."; - } - } - } - } - } - - my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'unbanmes'); - if ($channel_data->{unbanmes} <= 2) { - $channel_data->{unbanmes}++; - $self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data); - } - - $unbanned->{$channel}->{$mask} = $channel_data->{unbanmes}; - } - } - - if (keys %$unbanned) { - my $channels = ''; - - my $sep = ''; - my $channels_warning = ''; - my $sep_warning = ''; - my $channels_disabled = ''; - my $sep_disabled = ''; - - foreach my $channel (keys %$unbanned) { - foreach my $mask (keys %{$unbanned->{$channel}}) { - if ($self->{pbot}->{channels}->is_active_op("${channel}-floodbans")) { - if ($unbanned->{$channel}->{$mask} <= 2) { - $self->{pbot}->{banlist}->unban_user($channel . '-floodbans', 'b', $mask); - $channels .= "$sep$channel"; - $sep = ", "; - } - - if ($unbanned->{$channel}->{$mask} == 1) { - $channels_warning .= "$sep_warning$channel"; - $sep_warning = ", "; - } else { - $channels_disabled .= "$sep_disabled$channel"; - $sep_disabled = ", "; - } - } - } - } - - $self->{pbot}->{banlist}->flush_unban_queue(); - - $channels =~ s/(.*), /$1 and /; - $channels_warning =~ s/(.*), /$1 and /; - $channels_disabled =~ s/(.*), /$1 and /; - - my $warning = ''; - - if (length $channels_warning) { - $warning = - " You may use `unbanme` one more time today for $channels_warning; please ensure that your client or connection issues are resolved."; - } - - if (length $channels_disabled) { - $warning .= - " You may not use `unbanme` again for several hours for $channels_disabled."; - } - - if (length $channels) { return "/msg $context->{nick} You have been unbanned from $channels.$warning"; } - else { return "/msg $context->{nick} $warning"; } - } else { - return "/msg $context->{nick} There is no join-flooding ban set for you."; - } -} - -sub ban_exempted { - my ($self, $channel, $hostmask) = @_; - $channel = lc $channel; - $hostmask = lc $hostmask; - return 1 if $self->{'ban-exemptions'}->exists($channel, $hostmask); - return 0; -} - sub update_join_watch { my ($self, $account, $channel, $text, $mode) = @_; @@ -829,7 +659,7 @@ sub check_bans { my $tnickserv = defined $nickserv ? $nickserv : "[undefined]"; $self->{pbot}->{logger}->log("anti-flood: [check-bans] checking blacklist for $alias in channel $channel using gecos '$tgecos' and nickserv '$tnickserv'\n") if $debug_checkban >= 5; - if ($self->{pbot}->{blacklist}->check_blacklist($alias, $channel, $nickserv, $gecos)) { + if ($self->{pbot}->{blacklist}->is_blacklisted($alias, $channel, $nickserv, $gecos)) { my $u = $self->{pbot}->{users}->loggedin($channel, $mask); if ($self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted')) { $self->{pbot}->{logger}->log("anti-flood: [check-bans] $mask [$alias] blacklisted in $channel, but allowed through whitelist\n"); @@ -867,7 +697,7 @@ sub check_bans { my $u = $self->{pbot}->{users}->loggedin($baninfo->{channel}, $mask); my $whitelisted = $self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted'); - if ($self->ban_exempted($baninfo->{channel}, $baninfo->{mask}) || $whitelisted) { + if ($self->{pbot}->{banlist}->ban_exempted($baninfo->{channel}, $baninfo->{mask}) || $whitelisted) { #$self->{pbot}->{logger}->log("anti-flood: [check-bans] $mask [$alias] evaded $baninfo->{mask} in $baninfo->{channel}, but allowed through whitelist\n"); next; } diff --git a/lib/PBot/Core/AntiSpam.pm b/lib/PBot/Core/AntiSpam.pm index 86977b7a..e6e52ede 100644 --- a/lib/PBot/Core/AntiSpam.pm +++ b/lib/PBot/Core/AntiSpam.pm @@ -10,105 +10,20 @@ use parent 'PBot::Core::Class'; use PBot::Imports; -use Time::HiRes qw(gettimeofday); -use POSIX qw/strftime/; - sub initialize { my ($self, %conf) = @_; - my $filename = $conf{spamkeywords_file} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/spam_keywords'; - $self->{keywords} = PBot::Storage::DualIndexHashObject->new(name => 'SpamKeywords', filename => $filename, pbot => $self->{pbot}); + + my $filename = $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/spam_keywords'; + + $self->{keywords} = PBot::Storage::DualIndexHashObject->new( + pbot => $self->{pbot}, + name => 'SpamKeywords', + filename => $filename, + ); + $self->{keywords}->load; $self->{pbot}->{registry}->add_default('text', 'antispam', 'enforce', $conf{enforce_antispam} // 1); - $self->{pbot}->{commands}->register(sub { $self->cmd_antispam(@_) }, "antispam", 1); - $self->{pbot}->{capabilities}->add('admin', 'can-antispam', 1); -} - -sub cmd_antispam { - my ($self, $context) = @_; - - my $arglist = $context->{arglist}; - - my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); - - return "Usage: antispam , where commands are: list/show, add, remove, set, unset" if not defined $command; - - given ($command) { - when ($_ eq "list" or $_ eq "show") { - my $text = "Spam keywords:\n"; - my $entries = 0; - foreach my $namespace ($self->{keywords}->get_keys) { - $text .= ' ' . $self->{keywords}->get_key_name($namespace) . ":\n"; - foreach my $keyword ($self->{keywords}->get_keys($namespace)) { - $text .= ' ' . $self->{keywords}->get_key_name($namespace, $keyword) . ",\n"; - $entries++; - } - } - $text .= "none" if $entries == 0; - return $text; - } - when ("set") { - my ($namespace, $keyword, $flag, $value) = $self->{pbot}->{interpreter}->split_args($arglist, 4); - return "Usage: antispam set [flag [value]]" if not defined $namespace or not defined $keyword; - - if (not $self->{keywords}->exists($namespace)) { return "There is no such namespace `$namespace`."; } - - if (not $self->{keywords}->exists($namespace, $keyword)) { - return "There is no such regex `$keyword` for namespace `" . $self->{keywords}->get_key_name($namespace) . '`.'; - } - - if (not defined $flag) { - my $text = "Flags:\n"; - my $comma = ''; - foreach $flag ($self->{keywords}->get_keys($namespace, $keyword)) { - if ($flag eq 'created_on') { - my $timestamp = strftime "%a %b %e %H:%M:%S %Z %Y", localtime $self->{keywords}->get_data($namespace, $keyword, $flag); - $text .= $comma . "created_on: $timestamp"; - } else { - $value = $self->{keywords}->get_data($namespace, $keyword, $flag); - $text .= $comma . "$flag: $value"; - } - $comma = ",\n "; - } - return $text; - } - - if (not defined $value) { - $value = $self->{keywords}->get_data($namespace, $keyword, $flag); - if (not defined $value) { return "/say $flag is not set."; } - else { return "/say $flag is set to $value"; } - } - $self->{keywords}->set($namespace, $keyword, $flag, $value); - return "Flag set."; - } - when ("unset") { - my ($namespace, $keyword, $flag) = $self->{pbot}->{interpreter}->split_args($arglist, 3); - return "Usage: antispam unset " if not defined $namespace or not defined $keyword or not defined $flag; - - if (not $self->{keywords}->exists($namespace)) { return "There is no such namespace `$namespace`."; } - - if (not $self->{keywords}->exists($namespace, $keyword)) { return "There is no such keyword `$keyword` for namespace `$namespace`."; } - - if (not $self->{keywords}->exists($namespace, $keyword, $flag)) { return "There is no such flag `$flag` for regex `$keyword` for namespace `$namespace`."; } - return $self->{keywords}->remove($namespace, $keyword, $flag); - } - when ("add") { - my ($namespace, $keyword) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: antispam add " if not defined $namespace or not defined $keyword; - my $data = { - owner => $context->{hostmask}, - created_on => scalar gettimeofday - }; - $self->{keywords}->add($namespace, $keyword, $data); - return "/say Added `$keyword`."; - } - when ("remove") { - my ($namespace, $keyword) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: antispam remove " if not defined $namespace or not defined $keyword; - return $self->{keywords}->remove($namespace, $keyword); - } - default { return "Unknown command '$command'; commands are: list/show, add, remove"; } - } } sub is_spam { @@ -121,7 +36,9 @@ sub is_spam { my $ret = eval { foreach my $space ($self->{keywords}->get_keys) { if ($all_namespaces or $lc_namespace eq $space) { - foreach my $keyword ($self->{keywords}->get_keys($space)) { return 1 if $text =~ m/$keyword/i; } + foreach my $keyword ($self->{keywords}->get_keys($space)) { + return 1 if $text =~ m/$keyword/i; + } } } return 0; @@ -131,6 +48,7 @@ sub is_spam { $self->{pbot}->{logger}->log("Error in is_spam: $@"); return 0; } + $self->{pbot}->{logger}->log("AntiSpam: spam detected!\n") if $ret; return $ret; } diff --git a/lib/PBot/Core/BanList.pm b/lib/PBot/Core/BanList.pm index a38d8fcd..f1095a08 100644 --- a/lib/PBot/Core/BanList.pm +++ b/lib/PBot/Core/BanList.pm @@ -31,24 +31,33 @@ sub initialize { $self->{pbot}->{event_dispatcher}->register_handler('irc.endofbanlist', sub { $self->compare_banlist(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.endofquietlist', sub { $self->compare_quietlist(@_) }); + my $data_dir = $self->{pbot}->{registry}->get_value('general', 'data_dir'); + + $self->{'ban-exemptions'} = PBot::Storage::DualIndexHashObject->new( + pbot => $self->{pbot}, + name => 'Ban exemptions', + filename => "$data_dir/ban-exemptions", + ); + $self->{banlist} = PBot::Storage::DualIndexHashObject->new( pbot => $self->{pbot}, name => 'Ban List', - filename => $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/banlist', + filename => "$data_dir/banlist", save_queue_timeout => 15, ); $self->{quietlist} = PBot::Storage::DualIndexHashObject->new( pbot => $self->{pbot}, name => 'Quiet List', - filename => $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/quietlist', + filename => "$data_dir/quietlist", save_queue_timeout => 15, ); + $self->{'ban-exemptions'}->load; $self->{banlist}->load; $self->{quietlist}->load; - $self->enqueue_timeouts($self->{banlist}, 'b'); + $self->enqueue_timeouts($self->{banlist}, 'b'); $self->enqueue_timeouts($self->{quietlist}, $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char')); $self->{ban_queue} = {}; @@ -276,6 +285,14 @@ sub track_mode { } } +sub ban_exempted { + my ($self, $channel, $hostmask) = @_; + $channel = lc $channel; + $hostmask = lc $hostmask; + return 1 if $self->{'ban-exemptions'}->exists($channel, $hostmask); + return 0; +} + sub ban_user { my ($self, $channel, $mode, $mask, $immediately) = @_; $mode ||= 'b'; diff --git a/lib/PBot/Core/BlackList.pm b/lib/PBot/Core/BlackList.pm index 9f27a5da..93eca05d 100644 --- a/lib/PBot/Core/BlackList.pm +++ b/lib/PBot/Core/BlackList.pm @@ -10,111 +10,52 @@ use parent 'PBot::Core::Class'; use PBot::Imports; -use Time::HiRes qw(gettimeofday); - sub initialize { my ($self, %conf) = @_; - $self->{filename} = $conf{filename}; - $self->{blacklist} = {}; - $self->{pbot}->{commands}->register(sub { $self->cmd_blacklist(@_) }, "blacklist", 1); - $self->{pbot}->{capabilities}->add('admin', 'can-blacklist', 1); - $self->load_blacklist; -} - -sub cmd_blacklist { - my ($self, $context) = @_; - - my $arglist = $context->{arglist}; - $self->{pbot}->{interpreter}->lc_args($arglist); - - my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); - - return "Usage: blacklist , where commands are: list/show, add, remove" if not defined $command; - - given ($command) { - when ($_ eq "list" or $_ eq "show") { - my $text = "Blacklist:\n"; - my $entries = 0; - foreach my $channel (sort keys %{$self->{blacklist}}) { - if ($channel eq '.*') { $text .= " all channels:\n"; } - else { $text .= " $channel:\n"; } - foreach my $mask (sort keys %{$self->{blacklist}->{$channel}}) { - $text .= " $mask,\n"; - $entries++; - } - } - $text .= "none" if $entries == 0; - return "/msg $context->{nick} $text"; - } - when ("add") { - my ($mask, $channel) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: blacklist add [channel]" if not defined $mask; - - $channel = '.*' if not defined $channel; - - $self->{pbot}->{logger}->log("$context->{hostmask} added [$mask] to blacklist for channel [$channel]\n"); - $self->add($channel, $mask); - return "/say $mask blacklisted in channel $channel"; - } - when ("remove") { - my ($mask, $channel) = $self->{pbot}->{interpreter}->split_args($arglist, 2); - return "Usage: blacklist remove [channel]" if not defined $mask; - - $channel = '.*' if not defined $channel; - - if (exists $self->{blacklist}->{$channel} and not exists $self->{blacklist}->{$channel}->{$mask}) { - $self->{pbot}->{logger}->log("$context->{hostmask} attempt to remove nonexistent [$mask][$channel] from blacklist\n"); - return "/say $mask not found in blacklist for channel $channel (use `blacklist list` to display blacklist)"; - } - - $self->remove($channel, $mask); - $self->{pbot}->{logger}->log("$context->{hostmask} removed [$mask] from blacklist for channel [$channel]\n"); - return "/say $mask removed from blacklist for channel $channel"; - } - default { return "Unknown command '$command'; commands are: list/show, add, remove"; } - } + $self->{filename} = $conf{filename}; + $self->{storage} = {}; + $self->load; } sub add { my ($self, $channel, $hostmask) = @_; - $self->{blacklist}->{lc $channel}->{lc $hostmask} = 1; - $self->save_blacklist(); + $self->{storage}->{lc $channel}->{lc $hostmask} = 1; + $self->save; } sub remove { - my $self = shift; - my ($channel, $hostmask) = @_; + my ($self, $channel, $hostmask) = @_; $channel = lc $channel; $hostmask = lc $hostmask; - if (exists $self->{blacklist}->{$channel}) { - delete $self->{blacklist}->{$channel}->{$hostmask}; + if (exists $self->{storage}->{$channel}) { + delete $self->{storage}->{$channel}->{$hostmask}; - if (keys %{$self->{blacklist}->{$channel}} == 0) { delete $self->{blacklist}->{$channel}; } + if (not keys %{$self->{storage}->{$channel}}) { + delete $self->{storage}->{$channel}; + } } - $self->save_blacklist(); + + $self->save; } -sub clear_blacklist { - my $self = shift; - $self->{blacklist} = {}; +sub clear { + my ($self) = @_; + $self->{storage} = {}; } -sub load_blacklist { - my $self = shift; - my $filename; - if (@_) { $filename = shift; } - else { $filename = $self->{filename}; } +sub load { + my ($self) = @_; - if (not defined $filename) { + if (not $self->{filename}) { $self->{pbot}->{logger}->log("No blacklist path specified -- skipping loading of blacklist"); return; } - $self->{pbot}->{logger}->log("Loading blacklist from $filename ...\n"); + $self->{pbot}->{logger}->log("Loading blacklist from $self->{filename} ...\n"); - open(FILE, "< $filename") or Carp::croak "Couldn't open $filename: $!\n"; + open(FILE, "< $self->{filename}") or Carp::croak "Couldn't open $self->{filename}: $!\n"; my @contents = ; close(FILE); @@ -126,51 +67,55 @@ sub load_blacklist { my ($channel, $hostmask) = split(/\s+/, $line); - if (not defined $hostmask || not defined $channel) { Carp::croak "Syntax error around line $i of $filename\n"; } + if (not defined $hostmask || not defined $channel) { + Carp::croak "Syntax error around line $i of $self->{filename}\n"; + } - if (exists $self->{blacklist}->{$channel}->{$hostmask}) { Carp::croak "Duplicate blacklist entry [$hostmask][$channel] found in $filename around line $i\n"; } + if (exists $self->{storage}->{$channel}->{$hostmask}) { + Carp::croak "Duplicate blacklist entry $hostmask $channel found in $self->{filename} around line $i\n"; + } - $self->{blacklist}->{$channel}->{$hostmask} = 1; + $self->{storage}->{$channel}->{$hostmask} = 1; } $self->{pbot}->{logger}->log(" $i entries in blacklist\n"); } -sub save_blacklist { - my $self = shift; - my $filename; +sub save { + my ($self) = @_; - if (@_) { $filename = shift; } - else { $filename = $self->{filename}; } - - if (not defined $filename) { + if (not $self->{filename}) { $self->{pbot}->{logger}->log("No blacklist path specified -- skipping saving of blacklist\n"); return; } - open(FILE, "> $filename") or die "Couldn't open $filename: $!\n"; + open(FILE, "> $self->{filename}") or die "Couldn't open $self->{filename}: $!\n"; - foreach my $channel (keys %{$self->{blacklist}}) { - foreach my $hostmask (keys %{$self->{blacklist}->{$channel}}) { print FILE "$channel $hostmask\n"; } + foreach my $channel (keys %{$self->{storage}}) { + foreach my $hostmask (keys %{$self->{storage}->{$channel}}) { + print FILE "$channel $hostmask\n"; + } } - close(FILE); + close FILE; } -sub check_blacklist { - my $self = shift; - my ($hostmask, $channel, $nickserv, $gecos) = @_; +sub is_blacklisted { + my ($self, $hostmask, $channel, $nickserv, $gecos) = @_; return 0 if not defined $channel; my $result = eval { - foreach my $black_channel (keys %{$self->{blacklist}}) { - foreach my $black_hostmask (keys %{$self->{blacklist}->{$black_channel}}) { - my $flag = ''; - $flag = $1 if $black_hostmask =~ s/^\$(.)://; - + foreach my $black_channel (keys %{$self->{storage}}) { + foreach my $black_hostmask (keys %{$self->{storage}->{$black_channel}}) { next if $channel !~ /^$black_channel$/i; + my $flag = ''; + + if ($black_hostmask =~ s/^\$(.)://) { + $flag = $1; + } + if ($flag eq 'a' && defined $nickserv && $nickserv =~ /^$black_hostmask$/i) { $self->{pbot}->{logger}->log("$hostmask nickserv $nickserv blacklisted in channel $channel (matches [\$a:$black_hostmask] host and [$black_channel] channel)\n"); return 1; @@ -186,8 +131,8 @@ sub check_blacklist { return 0; }; - if ($@) { - $self->{pbot}->{logger}->log("Error in blacklist: $@\n"); + if (my $exception = $@) { + $self->{pbot}->{logger}->log("Error in blacklist: $exception"); return 0; } diff --git a/lib/PBot/Core/Commands/AntiSpam.pm b/lib/PBot/Core/Commands/AntiSpam.pm new file mode 100644 index 00000000..af688bca --- /dev/null +++ b/lib/PBot/Core/Commands/AntiSpam.pm @@ -0,0 +1,164 @@ +# File: AntiSpam.pm +# +# Purpose: Command to manipulate anti-spam list. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package PBot::Core::Commands::AntiSpam; +use parent 'PBot::Core::Class'; + +use PBot::Imports; + +use Time::HiRes qw/gettimeofday/; +use POSIX qw/strftime/; + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot}->{commands}->register(sub { $self->cmd_antispam(@_) }, "antispam", 1); + + # add capability to admin group + $self->{pbot}->{capabilities}->add('admin', 'can-antispam', 1); +} + +sub cmd_antispam { + my ($self, $context) = @_; + + my $arglist = $context->{arglist}; + + my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); + + if (not defined $command) { + return "Usage: antispam , where commands are: list/show, add, remove, set, unset"; + } + + my $keywords = $self->{pbot}->{antispam}->{keywords}; + + given ($command) { + when ($_ eq "list" or $_ eq "show") { + my $text = "Spam keywords:\n"; + my $entries = 0; + + foreach my $namespace ($keywords->get_keys) { + $text .= ' ' . $keywords->get_key_name($namespace) . ":\n"; + + foreach my $keyword ($keywords->get_keys($namespace)) { + $text .= ' ' . $keywords->get_key_name($namespace, $keyword) . ",\n"; + $entries++; + } + } + + $text .= "none" if $entries == 0; + return $text; + } + + when ("set") { + my ($namespace, $keyword, $flag, $value) = $self->{pbot}->{interpreter}->split_args($arglist, 4); + + if (not defined $namespace or not defined $keyword) { + return "Usage: antispam set [flag [value]]" + } + + if (not $keywords->exists($namespace)) { + return "There is no such namespace `$namespace`."; + } + + if (not $keywords->exists($namespace, $keyword)) { + return "There is no such regex `$keyword` for namespace `" . $keywords->get_key_name($namespace) . '`.'; + } + + if (not defined $flag) { + my @flags; + + foreach $flag ($keywords->get_keys($namespace, $keyword)) { + if ($flag eq 'created_on') { + my $timestamp = strftime "%a %b %e %H:%M:%S %Z %Y", localtime $keywords->get_data($namespace, $keyword, $flag); + push @flags, "created_on: $timestamp"; + } else { + $value = $keywords->get_data($namespace, $keyword, $flag); + push @flags, "$flag: $value"; + } + + } + + my $text = "Flags: "; + + if (@flags) { + $text .= join ",\n", @flags; + } else { + $text .= 'none'; + } + + return $text; + } + + if (not defined $value) { + $value = $keywords->get_data($namespace, $keyword, $flag); + + if (not defined $value) { + return "/say $flag is not set."; + } else { + return "/say $flag is set to $value"; + } + } + + $keywords->set($namespace, $keyword, $flag, $value); + return "Flag set."; + } + + when ("unset") { + my ($namespace, $keyword, $flag) = $self->{pbot}->{interpreter}->split_args($arglist, 3); + + if (not defined $namespace or not defined $keyword or not defined $flag) { + return "Usage: antispam unset " + } + + if (not $keywords->exists($namespace)) { + return "There is no such namespace `$namespace`."; + } + + if (not $keywords->exists($namespace, $keyword)) { + return "There is no such keyword `$keyword` for namespace `$namespace`."; + } + + if (not $keywords->exists($namespace, $keyword, $flag)) { + return "There is no such flag `$flag` for regex `$keyword` for namespace `$namespace`."; + } + + return $keywords->remove($namespace, $keyword, $flag); + } + + when ("add") { + my ($namespace, $keyword) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + + if (not defined $namespace or not defined $keyword) { + return "Usage: antispam add "; + } + + my $data = { + owner => $context->{hostmask}, + created_on => scalar gettimeofday + }; + + $keywords->add($namespace, $keyword, $data); + return "/say Added `$keyword`."; + } + + when ("remove") { + my ($namespace, $keyword) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + + if (not defined $namespace or not defined $keyword) { + return "Usage: antispam remove "; + } + + return $keywords->remove($namespace, $keyword); + } + + default { + return "Unknown command '$command'; commands are: list/show, add, remove"; + } + } +} + +1; diff --git a/lib/PBot/Core/Commands/BanList.pm b/lib/PBot/Core/Commands/BanList.pm index fd638cf6..e3588a76 100644 --- a/lib/PBot/Core/Commands/BanList.pm +++ b/lib/PBot/Core/Commands/BanList.pm @@ -17,9 +17,15 @@ use POSIX qw/strftime/; sub initialize { my ($self, %conf) = @_; - $self->{pbot}->{commands}->register(sub { $self->cmd_banlist(@_) }, "banlist", 0); - $self->{pbot}->{commands}->register(sub { $self->cmd_checkban(@_) }, "checkban", 0); - $self->{pbot}->{commands}->register(sub { $self->cmd_checkmute(@_) }, "checkmute", 0); + $self->{pbot}->{commands}->register(sub { $self->cmd_banlist(@_) }, "banlist", 0); + $self->{pbot}->{commands}->register(sub { $self->cmd_checkban(@_) }, "checkban", 0); + $self->{pbot}->{commands}->register(sub { $self->cmd_checkmute(@_) }, "checkmute", 0); + $self->{pbot}->{commands}->register(sub { $self->cmd_unbanme(@_) }, "unbanme", 0); + $self->{pbot}->{commands}->register(sub { $self->cmd_ban_exempt(@_) }, "ban-exempt", 1); + + # add capability to admin group + $self->{pbot}->{capabilities}->add('admin', 'can-ban-exempt', 1); + } sub cmd_banlist { @@ -107,4 +113,158 @@ sub cmd_checkmute { return $self->{pbot}->{banlist}->checkban($channel, $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char'), $target); } +sub cmd_unbanme { + my ($self, $context) = @_; + my $unbanned; + + my %aliases = $self->{pbot}->{messagehistory}->{database}->get_also_known_as($context->{nick}); + + foreach my $alias (keys %aliases) { + next if $aliases{$alias}->{type} == $self->{pbot}->{messagehistory}->{database}->{alias_type}->{WEAK}; + next if $aliases{$alias}->{nickchange} == 1; + + my $join_flood_channel = $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_channel') // '#stop-join-flood'; + + my ($anick, $auser, $ahost) = $alias =~ m/([^!]+)!([^@]+)@(.*)/; + my $banmask = $self->{pbot}->{antiflood}->address_to_mask($ahost); + my $mask = "*!$auser\@$banmask\$$join_flood_channel"; + + my @channels = $self->{pbot}->{messagehistory}->{database}->get_channels($aliases{$alias}->{id}); + + foreach my $channel (@channels) { + next if exists $unbanned->{$channel} and exists $unbanned->{$channel}->{$mask}; + next if not $self->{pbot}->{banlist}->{banlist}->exists($channel . '-floodbans', $mask); + + my $message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account($anick, $auser, $ahost); + my @nickserv_accounts = $self->{pbot}->{messagehistory}->{database}->get_nickserv_accounts($message_account); + + push @nickserv_accounts, undef; + + foreach my $nickserv_account (@nickserv_accounts) { + my $baninfos = $self->{pbot}->{banlist}->get_baninfo($channel, "$anick!$auser\@$ahost", $nickserv_account); + + if (defined $baninfos) { + foreach my $baninfo (@$baninfos) { + my $u = $self->{pbot}->{users}->loggedin($baninfo->{channel}, $context->{hostmask}); + my $whitelisted = $self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted'); + if ($self->{pbot}->{banlist}->ban_exempted($baninfo->{channel}, $baninfo->{mask}) || $whitelisted) { + $self->{pbot}->{logger}->log("anti-flood: [unbanme] $anick!$auser\@$ahost banned as $baninfo->{mask} in $baninfo->{channel}, but allowed through whitelist\n"); + } else { + if ($channel eq lc $baninfo->{channel}) { + my $mode = $baninfo->{type} eq 'b' ? "banned" : "quieted"; + $self->{pbot}->{logger}->log("anti-flood: [unbanme] $anick!$auser\@$ahost $mode as $baninfo->{mask} in $baninfo->{channel} by $baninfo->{owner}, unbanme rejected\n"); + return "/msg $context->{nick} You have been $mode as $baninfo->{mask} by $baninfo->{owner}, unbanme will not work until it is removed."; + } + } + } + } + } + + my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'unbanmes'); + if ($channel_data->{unbanmes} <= 2) { + $channel_data->{unbanmes}++; + $self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data); + } + + $unbanned->{$channel}->{$mask} = $channel_data->{unbanmes}; + } + } + + if (keys %$unbanned) { + my $channels = ''; + + my $sep = ''; + my $channels_warning = ''; + my $sep_warning = ''; + my $channels_disabled = ''; + my $sep_disabled = ''; + + foreach my $channel (keys %$unbanned) { + foreach my $mask (keys %{$unbanned->{$channel}}) { + if ($self->{pbot}->{channels}->is_active_op("${channel}-floodbans")) { + if ($unbanned->{$channel}->{$mask} <= 2) { + $self->{pbot}->{banlist}->unban_user($channel . '-floodbans', 'b', $mask); + $channels .= "$sep$channel"; + $sep = ", "; + } + + if ($unbanned->{$channel}->{$mask} == 1) { + $channels_warning .= "$sep_warning$channel"; + $sep_warning = ", "; + } else { + $channels_disabled .= "$sep_disabled$channel"; + $sep_disabled = ", "; + } + } + } + } + + $self->{pbot}->{banlist}->flush_unban_queue(); + + $channels =~ s/(.*), /$1 and /; + $channels_warning =~ s/(.*), /$1 and /; + $channels_disabled =~ s/(.*), /$1 and /; + + my $warning = ''; + + if (length $channels_warning) { + $warning = + " You may use `unbanme` one more time today for $channels_warning; please ensure that your client or connection issues are resolved."; + } + + if (length $channels_disabled) { + $warning .= + " You may not use `unbanme` again for several hours for $channels_disabled."; + } + + if (length $channels) { return "/msg $context->{nick} You have been unbanned from $channels.$warning"; } + else { return "/msg $context->{nick} $warning"; } + } else { + return "/msg $context->{nick} There is no join-flooding ban set for you."; + } +} + +sub cmd_ban_exempt { + my ($self, $context) = @_; + my $arglist = $context->{arglist}; + $self->{pbot}->{interpreter}->lc_args($arglist); + + my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); + return "Usage: ban-exempt , where commands are: list, add, remove" if not defined $command; + + given ($command) { + when ($_ eq 'list') { + my $text = "Ban-evasion exemptions:\n"; + my $entries = 0; + foreach my $channel ($self->{pbot}->{banlist}->{'ban-exemptions'}->get_keys) { + $text .= ' ' . $self->{pbot}->{banlist}->{'ban-exemptions'}->get_key_name($channel) . ":\n"; + foreach my $mask ($self->{pbot}->{banlist}->{'ban-exemptions'}->get_keys($channel)) { + $text .= " $mask,\n"; + $entries++; + } + } + $text .= "none" if $entries == 0; + return $text; + } + when ("add") { + my ($channel, $mask) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + return "Usage: ban-exempt add " if not defined $channel or not defined $mask; + + my $data = { + owner => $context->{hostmask}, + created_on => scalar gettimeofday + }; + + $self->{pbot}->{banlist}->{'ban-exemptions'}->add($channel, $mask, $data); + return "/say $mask exempted from ban-evasions in channel $channel"; + } + when ("remove") { + my ($channel, $mask) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + return "Usage: ban-exempt remove " if not defined $channel or not defined $mask; + return $self->{pbot}->{banlist}->{'ban-exemptions'}->remove($channel, $mask); + } + default { return "Unknown command '$command'; commands are: list, add, remove"; } + } +} + 1; diff --git a/lib/PBot/Core/Commands/BlackList.pm b/lib/PBot/Core/Commands/BlackList.pm new file mode 100644 index 00000000..663e76c7 --- /dev/null +++ b/lib/PBot/Core/Commands/BlackList.pm @@ -0,0 +1,97 @@ +# File: BlackList.pm +# +# Purpose: Command to manage list of hostmasks that are not allowed +# to join a channel. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package PBot::Core::Commands::BlackList; +use parent 'PBot::Core::Class'; + +use PBot::Imports; + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot}->{commands}->register(sub { $self->cmd_blacklist(@_) }, "blacklist", 1); + + # add capability to admin group + $self->{pbot}->{capabilities}->add('admin', 'can-blacklist', 1); +} + +sub cmd_blacklist { + my ($self, $context) = @_; + + my $arglist = $context->{arglist}; + $self->{pbot}->{interpreter}->lc_args($arglist); + + my $command = $self->{pbot}->{interpreter}->shift_arg($arglist); + + if (not defined $command) { + return "Usage: blacklist , where commands are: list/show, add, remove"; + } + + my $blacklist = $self->{pbot}->{blacklist}->{storage}; + + given ($command) { + when ($_ eq "list" or $_ eq "show") { + my $text = "Blacklist:\n"; + my $entries = 0; + + foreach my $channel (sort keys %$blacklist) { + if ($channel eq '.*') { + $text .= " all channels:\n"; + } else { + $text .= " $channel:\n"; + } + + foreach my $mask (sort keys %{$blacklist->{$channel}}) { + $text .= " $mask,\n"; + $entries++; + } + } + + $text .= "none" if $entries == 0; + return "/msg $context->{nick} $text"; + } + + when ("add") { + my ($mask, $channel) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + + if (not defined $mask) { + return "Usage: blacklist add [channel]"; + } + + $channel = '.*' if not defined $channel; + + $self->{pbot}->{logger}->log("$context->{hostmask} added [$mask] to blacklist for channel [$channel]\n"); + $self->add($channel, $mask); + return "/say $mask blacklisted in channel $channel"; + } + + when ("remove") { + my ($mask, $channel) = $self->{pbot}->{interpreter}->split_args($arglist, 2); + + if (not defined $mask) { + return "Usage: blacklist remove [channel]"; + } + + $channel = '.*' if not defined $channel; + + if (exists $blacklist->{$channel} and not exists $blacklist->{$channel}->{$mask}) { + return "/say $mask not found in blacklist for channel $channel (use `blacklist list` to display blacklist)"; + } + + $self->remove($channel, $mask); + $self->{pbot}->{logger}->log("$context->{hostmask} removed $mask from blacklist for channel $channel\n"); + return "/say $mask removed from blacklist for channel $channel"; + } + + default { + return "Unknown command '$command'; commands are: list/show, add, remove"; + } + } +} + +1; diff --git a/lib/PBot/Core/Commands/Func.pm b/lib/PBot/Core/Commands/Func.pm new file mode 100644 index 00000000..70270fa9 --- /dev/null +++ b/lib/PBot/Core/Commands/Func.pm @@ -0,0 +1,56 @@ +# File: Func.pm +# +# Purpose: Special `func` command that executes built-in functions with +# optional arguments. Usage: func [arguments]. +# +# Intended usage is with command-substitution (&{}) or pipes (|{}). +# +# For example: +# +# factadd img /call echo https://google.com/search?q=&{func uri_escape $args}&tbm=isch +# +# The above would invoke the function 'uri_escape' on $args and then replace +# the command-substitution with the result, thus escaping $args to be safely +# used in the URL of this simple Google Image Search factoid command. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package PBot::Core::Commands::Func; +use parent 'PBot::Core::Class'; + +use PBot::Imports; + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot}->{commands}->register(sub { $self->cmd_func(@_) }, 'func', 0); +} + +sub cmd_func { + my ($self, $context) = @_; + + my $func = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); + + if (not defined $func) { + return "Usage: func [arguments]; see also: func help"; + } + + if (not exists $self->{pbot}->{functions}->{funcs}->{$func}) { + return "[No such func '$func']" + } + + my @params; + + while (defined(my $param = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}))) { + push @params, $param; + } + + my $result = $self->{pbot}->{functions}->{funcs}->{$func}->{subref}->(@params); + + $result =~ s/\x1/1/g; # strip CTCP code + + return $result; +} + +1; diff --git a/lib/PBot/Core/Commands/IgnoreList.pm b/lib/PBot/Core/Commands/IgnoreList.pm new file mode 100644 index 00000000..722fc5f5 --- /dev/null +++ b/lib/PBot/Core/Commands/IgnoreList.pm @@ -0,0 +1,101 @@ +# File: IgnoreList.pm +# +# Purpose: Commands to manage ignore list. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package PBot::Core::Commands::IgnoreList; +use parent 'PBot::Core::Class'; + +use PBot::Imports; + +use Time::Duration qw/concise duration/; + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot}->{commands}->register(sub { $self->cmd_ignore(@_) }, "ignore", 1); + $self->{pbot}->{commands}->register(sub { $self->cmd_unignore(@_) }, "unignore", 1); + + # add capabilites to admin group + $self->{pbot}->{capabilities}->add('admin', 'can-ignore', 1); + $self->{pbot}->{capabilities}->add('admin', 'can-unignore', 1); + + # add capabilities to chanop group + $self->{pbot}->{capabilities}->add('chanop', 'can-ignore', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-unignore', 1); +} + +sub cmd_ignore { + my ($self, $context) = @_; + + my ($target, $channel, $length) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3); + + if (not defined $target) { + return "Usage: ignore [channel [timeout]] | ignore list"; + } + + if ($target =~ /^list$/i) { + my $text = "Ignored:\n\n"; + my $now = time; + my $ignored = 0; + + my $ignorelist = $self->{pbot}->{ignorelist}->{storage}; + + foreach my $channel (sort $ignorelist->get_keys) { + $text .= $channel eq '.*' ? "global:\n" : "$channel:\n"; + + my @list; + foreach my $hostmask (sort $ignorelist->get_keys($channel)) { + my $timeout = $ignorelist->get_data($channel, $hostmask, 'timeout'); + + if ($timeout == -1) { + push @list, " $hostmask"; + } else { + push @list, " $hostmask (" . (concise duration $timeout - $now) . ')'; + } + + $ignored++; + } + + $text .= join ";\n", @list; + $text .= "\n"; + } + + return "Ignore list is empty." if not $ignored; + return "/msg $context->{nick} $text"; + } + + if (not defined $channel) { + $channel = ".*"; # all channels + } + + if (not defined $length) { + $length = -1; # permanently + } else { + my $error; + ($length, $error) = $self->{pbot}->{parsedate}->parsedate($length); + return $error if defined $error; + } + + return $self->{pbot}->{ignorelist}->add($channel, $target, $length, $context->{hostmask}); +} + +sub cmd_unignore { + my ($self, $context) = @_; + + my ($target, $channel) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); + + if (not defined $target) { + return "Usage: unignore [channel]"; + } + + if (not defined $channel) { + $channel = '.*'; + } + + return $self->{pbot}->{ignorelist}->remove($channel, $target); +} + +1; diff --git a/lib/PBot/Core/Commands/Misc.pm b/lib/PBot/Core/Commands/Misc.pm index 73db5980..18946c67 100644 --- a/lib/PBot/Core/Commands/Misc.pm +++ b/lib/PBot/Core/Commands/Misc.pm @@ -27,7 +27,6 @@ sub initialize { $self->{pbot}->{commands}->register(sub { $self->cmd_die(@_) }, 'die', 1); $self->{pbot}->{commands}->register(sub { $self->cmd_export(@_) }, 'export', 1); $self->{pbot}->{commands}->register(sub { $self->cmd_eval(@_) }, 'eval', 1); - $self->{pbot}->{commands}->register(sub { $self->cmd_reload(@_) }, 'reload', 1); # misc capabilities $self->{pbot}->{capabilities}->add('admin', 'can-in', 1); @@ -149,75 +148,4 @@ sub cmd_eval { return "/say $ret $result"; } -sub cmd_reload { - my ($self, $context) = @_; - - my %reloadables = ( - 'capabilities' => sub { - $self->{pbot}->{capabilities}->{caps}->load; - return "Capabilities reloaded."; - }, - - 'commands' => sub { - $self->{pbot}->{commands}->{metadata}->load; - return "Commands metadata reloaded."; - }, - - 'blacklist' => sub { - $self->{pbot}->{blacklist}->clear_blacklist; - $self->{pbot}->{blacklist}->load_blacklist; - return "Blacklist reloaded."; - }, - - 'ban-exemptions' => sub { - $self->{pbot}->{antiflood}->{'ban-exemptions'}->load; - return "Ban exemptions reloaded."; - }, - - 'ignores' => sub { - $self->{pbot}->{ignorelist}->{storage}->load; - return "Ignore list reloaded."; - }, - - 'users' => sub { - $self->{pbot}->{users}->load; - return "Users reloaded."; - }, - - 'channels' => sub { - $self->{pbot}->{channels}->{storage}->load; - return "Channels reloaded."; - }, - - 'banlist' => sub { - $self->{pbot}->{event_queue}->dequeue_event('unban #.*'); - $self->{pbot}->{event_queue}->dequeue_event('unmute #.*'); - $self->{pbot}->{banlist}->{banlist}->load; - $self->{pbot}->{banlist}->{quietlist}->load; - $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{banlist}, 'b'); - $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{quietlist}, 'q'); - return "Ban list reloaded."; - }, - - 'registry' => sub { - $self->{pbot}->{registry}->load; - return "Registry reloaded."; - }, - - 'factoids' => sub { - $self->{pbot}->{factoids}->load_factoids; - return "Factoids reloaded."; - } - ); - - if (not length $context->{arguments} or not exists $reloadables{$context->{arguments}}) { - my $usage = 'Usage: reload <'; - $usage .= join '|', sort keys %reloadables; - $usage .= '>'; - return $usage; - } - - return $reloadables{$context->{arguments}}(); -} - 1; diff --git a/lib/PBot/Core/Commands/Reload.pm b/lib/PBot/Core/Commands/Reload.pm new file mode 100644 index 00000000..76722368 --- /dev/null +++ b/lib/PBot/Core/Commands/Reload.pm @@ -0,0 +1,90 @@ +# File: Reload.pm +# +# Purpose: Command to reload various PBot storage files. + +# SPDX-FileCopyrightText: 2021 Pragmatic Software +# SPDX-License-Identifier: MIT + +package PBot::Core::Commands::Reload; + +use PBot::Imports; +use parent 'PBot::Core::Class'; + +sub initialize { + my ($self, %conf) = @_; + + $self->{pbot}->{commands}->register(sub { $self->cmd_reload(@_) }, 'reload', 1); +} + +sub cmd_reload { + my ($self, $context) = @_; + + my %reloadables = ( + 'capabilities' => sub { + $self->{pbot}->{capabilities}->{caps}->load; + return "Capabilities reloaded."; + }, + + 'commands' => sub { + $self->{pbot}->{commands}->{metadata}->load; + return "Commands metadata reloaded."; + }, + + 'blacklist' => sub { + $self->{pbot}->{blacklist}->clear_blacklist; + $self->{pbot}->{blacklist}->load_blacklist; + return "Blacklist reloaded."; + }, + + 'ban-exemptions' => sub { + $self->{pbot}->{banlist}->{'ban-exemptions'}->load; + return "Ban exemptions reloaded."; + }, + + 'ignores' => sub { + $self->{pbot}->{ignorelist}->{storage}->load; + return "Ignore list reloaded."; + }, + + 'users' => sub { + $self->{pbot}->{users}->load; + return "Users reloaded."; + }, + + 'channels' => sub { + $self->{pbot}->{channels}->{storage}->load; + return "Channels reloaded."; + }, + + 'banlist' => sub { + $self->{pbot}->{event_queue}->dequeue_event('unban #.*'); + $self->{pbot}->{event_queue}->dequeue_event('unmute #.*'); + $self->{pbot}->{banlist}->{banlist}->load; + $self->{pbot}->{banlist}->{quietlist}->load; + $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{banlist}, 'b'); + $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{quietlist}, 'q'); + return "Ban list reloaded."; + }, + + 'registry' => sub { + $self->{pbot}->{registry}->load; + return "Registry reloaded."; + }, + + 'factoids' => sub { + $self->{pbot}->{factoids}->load_factoids; + return "Factoids reloaded."; + } + ); + + if (not length $context->{arguments} or not exists $reloadables{$context->{arguments}}) { + my $usage = 'Usage: reload <'; + $usage .= join '|', sort keys %reloadables; + $usage .= '>'; + return $usage; + } + + return $reloadables{$context->{arguments}}(); +} + +1; diff --git a/lib/PBot/Core/Functions.pm b/lib/PBot/Core/Functions.pm index 3eeaa6cb..3240a21d 100644 --- a/lib/PBot/Core/Functions.pm +++ b/lib/PBot/Core/Functions.pm @@ -12,6 +12,8 @@ # The above would invoke the function 'uri_escape' on $args and then replace # the command-substitution with the result, thus escaping $args to be safely # used in the URL of this simple Google Image Search factoid command. +# +# See also: Plugin/FuncBuiltins.pm, Plugin/FuncGrep.pm and Plugin/FuncSed.pm # SPDX-FileCopyrightText: 2021 Pragmatic Software # SPDX-License-Identifier: MIT @@ -24,16 +26,8 @@ use PBot::Imports; sub initialize { my ($self, %conf) = @_; - $self->{pbot}->{commands}->register(sub { $self->cmd_func(@_) }, 'func', 0); - - $self->register( - 'help', - { - desc => 'provides help about a func', - usage => 'help [func]', - subref => sub { $self->func_help(@_) } - } - ); + # register `list` and `help` functions used to list + # functions and obtain help about them $self->register( 'list', @@ -43,32 +37,15 @@ sub initialize { subref => sub { $self->func_list(@_) } } ); -} -sub cmd_func { - my ($self, $context) = @_; - - my $func = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); - - if (not defined $func) { - return "Usage: func [arguments]; see also: func help"; - } - - if (not exists $self->{funcs}->{$func}) { - return "[No such func '$func']" - } - - my @params; - - while (defined(my $param = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}))) { - push @params, $param; - } - - my $result = $self->{funcs}->{$func}->{subref}->(@params); - - $result =~ s/\x1/1/g; # strip CTCP code - - return $result; + $self->register( + 'help', + { + desc => 'provides help about a func', + usage => 'help [func]', + subref => sub { $self->func_help(@_) } + } + ); } sub register { @@ -81,20 +58,6 @@ sub unregister { delete $self->{funcs}->{$func}; } -sub func_help { - my ($self, $func) = @_; - - if (not length $func) { - return "func: invoke built-in functions; usage: func [arguments]; to list available functions: func list [regex]"; - } - - if (not exists $self->{funcs}->{$func}) { - return "No such func '$func'."; - } - - return "$func: $self->{funcs}->{$func}->{desc}; usage: $self->{funcs}->{$func}->{usage}"; -} - sub func_list { my ($self, $regex) = @_; @@ -131,4 +94,18 @@ sub func_list { return $result; } +sub func_help { + my ($self, $func) = @_; + + if (not length $func) { + return "func: invoke built-in functions; usage: func [arguments]; to list available functions: func list [regex]"; + } + + if (not exists $self->{funcs}->{$func}) { + return "No such func '$func'."; + } + + return "$func: $self->{funcs}->{$func}->{desc}; usage: $self->{funcs}->{$func}->{usage}"; +} + 1; diff --git a/lib/PBot/Core/IgnoreList.pm b/lib/PBot/Core/IgnoreList.pm index 0c5e9901..25e2c18e 100644 --- a/lib/PBot/Core/IgnoreList.pm +++ b/lib/PBot/Core/IgnoreList.pm @@ -10,79 +10,21 @@ use parent 'PBot::Core::Class'; use PBot::Imports; -use Time::Duration qw/concise duration/; +use Time::Duration qw/duration/; sub initialize { my ($self, %conf) = @_; $self->{filename} = $conf{filename}; - $self->{storage} = PBot::Storage::DualIndexHashObject->new(pbot => $self->{pbot}, name => 'IgnoreList', filename => $self->{filename}); + $self->{storage} = PBot::Storage::DualIndexHashObject->new( + pbot => $self->{pbot}, + name => 'IgnoreList', + filename => $self->{filename} + ); + $self->{storage}->load; $self->enqueue_ignores; - - $self->{pbot}->{commands}->register(sub { $self->cmd_ignore(@_) }, "ignore", 1); - $self->{pbot}->{commands}->register(sub { $self->cmd_unignore(@_) }, "unignore", 1); - - $self->{pbot}->{capabilities}->add('admin', 'can-ignore', 1); - $self->{pbot}->{capabilities}->add('admin', 'can-unignore', 1); - - $self->{pbot}->{capabilities}->add('chanop', 'can-ignore', 1); - $self->{pbot}->{capabilities}->add('chanop', 'can-unignore', 1); -} - -sub cmd_ignore { - my ($self, $context) = @_; - - my ($target, $channel, $length) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3); - - return "Usage: ignore [channel [timeout]] | ignore list" if not defined $target; - - if ($target =~ /^list$/i) { - my $text = "Ignored:\n\n"; - my $now = time; - my $ignored = 0; - - foreach my $channel (sort $self->{storage}->get_keys) { - $text .= $channel eq '.*' ? "global:\n" : "$channel:\n"; - my @list; - foreach my $hostmask (sort $self->{storage}->get_keys($channel)) { - my $timeout = $self->{storage}->get_data($channel, $hostmask, 'timeout'); - if ($timeout == -1) { - push @list, " $hostmask"; - } else { - push @list, " $hostmask (" . (concise duration $timeout - $now) . ')'; - } - $ignored++; - } - $text .= join ";\n", @list; - $text .= "\n"; - } - return "Ignore list is empty." if not $ignored; - return "/msg $context->{nick} $text"; - } - - if (not defined $channel) { - $channel = ".*"; # all channels - } - - if (not defined $length) { - $length = -1; # permanently - } else { - my $error; - ($length, $error) = $self->{pbot}->{parsedate}->parsedate($length); - return $error if defined $error; - } - - return $self->add($channel, $target, $length, $context->{hostmask}); -} - -sub cmd_unignore { - my ($self, $context) = @_; - my ($target, $channel) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); - if (not defined $target) { return "Usage: unignore [channel]"; } - if (not defined $channel) { $channel = '.*'; } - return $self->remove($channel, $target); } sub enqueue_ignores {