mirror of
https://github.com/pragma-/pbot.git
synced 2024-11-29 07:19:23 +01:00
Finish moving commands from Core to Commands
This commit is contained in:
parent
4b80131473
commit
4b3c5d58cf
@ -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 <command>, 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 <channel> <mask>" 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 <channel> <mask>" 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;
|
||||
}
|
||||
|
@ -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 <command>, 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 <namespace> <regex> [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 <namespace> <regex> <flag>" 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 <namespace> <regex>" 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 <namespace> <regex>" 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;
|
||||
}
|
||||
|
@ -31,20 +31,29 @@ 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;
|
||||
|
||||
@ -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';
|
||||
|
@ -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 <command>, 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 <hostmask regex> [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 <hostmask regex> [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->{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 = <FILE>;
|
||||
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;
|
||||
}
|
||||
|
||||
|
164
lib/PBot/Core/Commands/AntiSpam.pm
Normal file
164
lib/PBot/Core/Commands/AntiSpam.pm
Normal file
@ -0,0 +1,164 @@
|
||||
# File: AntiSpam.pm
|
||||
#
|
||||
# Purpose: Command to manipulate anti-spam list.
|
||||
|
||||
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
|
||||
# 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 <command>, 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 <namespace> <regex> [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 <namespace> <regex> <flag>"
|
||||
}
|
||||
|
||||
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 <namespace> <regex>";
|
||||
}
|
||||
|
||||
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 <namespace> <regex>";
|
||||
}
|
||||
|
||||
return $keywords->remove($namespace, $keyword);
|
||||
}
|
||||
|
||||
default {
|
||||
return "Unknown command '$command'; commands are: list/show, add, remove";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
@ -20,6 +20,12 @@ sub initialize {
|
||||
$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 <command>, 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 <channel> <mask>" 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 <channel> <mask>" 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;
|
||||
|
97
lib/PBot/Core/Commands/BlackList.pm
Normal file
97
lib/PBot/Core/Commands/BlackList.pm
Normal file
@ -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 <pragma78@gmail.com>
|
||||
# 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 <command>, 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 <hostmask regex> [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 <hostmask regex> [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;
|
56
lib/PBot/Core/Commands/Func.pm
Normal file
56
lib/PBot/Core/Commands/Func.pm
Normal file
@ -0,0 +1,56 @@
|
||||
# File: Func.pm
|
||||
#
|
||||
# Purpose: Special `func` command that executes built-in functions with
|
||||
# optional arguments. Usage: func <identifier> [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 <pragma78@gmail.com>
|
||||
# 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 <keyword> [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;
|
101
lib/PBot/Core/Commands/IgnoreList.pm
Normal file
101
lib/PBot/Core/Commands/IgnoreList.pm
Normal file
@ -0,0 +1,101 @@
|
||||
# File: IgnoreList.pm
|
||||
#
|
||||
# Purpose: Commands to manage ignore list.
|
||||
|
||||
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
|
||||
# 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 <hostmask> [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 <hostmask> [channel]";
|
||||
}
|
||||
|
||||
if (not defined $channel) {
|
||||
$channel = '.*';
|
||||
}
|
||||
|
||||
return $self->{pbot}->{ignorelist}->remove($channel, $target);
|
||||
}
|
||||
|
||||
1;
|
@ -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;
|
||||
|
90
lib/PBot/Core/Commands/Reload.pm
Normal file
90
lib/PBot/Core/Commands/Reload.pm
Normal file
@ -0,0 +1,90 @@
|
||||
# File: Reload.pm
|
||||
#
|
||||
# Purpose: Command to reload various PBot storage files.
|
||||
|
||||
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
|
||||
# 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;
|
@ -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 <pragma78@gmail.com>
|
||||
# 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 <keyword> [arguments]; see also: func help";
|
||||
$self->register(
|
||||
'help',
|
||||
{
|
||||
desc => 'provides help about a func',
|
||||
usage => 'help [func]',
|
||||
subref => sub { $self->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;
|
||||
);
|
||||
}
|
||||
|
||||
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 <keyword> [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 <keyword> [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;
|
||||
|
@ -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 <hostmask> [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 <hostmask> [channel]"; }
|
||||
if (not defined $channel) { $channel = '.*'; }
|
||||
return $self->remove($channel, $target);
|
||||
}
|
||||
|
||||
sub enqueue_ignores {
|
||||
|
Loading…
Reference in New Issue
Block a user