pbot/lib/PBot/Core/BanList.pm

493 lines
16 KiB
Perl
Raw Normal View History

# File: BanList.pm
2011-02-13 06:07:02 +01:00
#
# Purpose: Implements functions related to maintaining and tracking channel
# bans/mutes. Maintains ban/mute queues and timeouts.
2011-02-13 06:07:02 +01:00
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
2021-07-21 07:44:51 +02:00
package PBot::Core::BanList;
2020-02-15 23:38:32 +01:00
2021-07-21 07:44:51 +02:00
use parent 'PBot::Core::Class';
2011-02-13 06:07:02 +01:00
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2019-07-11 03:40:53 +02:00
2011-02-13 06:07:02 +01:00
use Time::HiRes qw/gettimeofday/;
use Time::Duration;
use POSIX qw/strftime/;
2020-02-15 23:38:32 +01:00
2011-02-13 06:07:02 +01:00
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
$self->{pbot}->{registry}->add_default('text', 'banlist', 'chanserv_ban_timeout', '604800');
$self->{pbot}->{registry}->add_default('text', 'banlist', 'mute_timeout', '604800');
$self->{pbot}->{registry}->add_default('text', 'banlist', 'debug', '0');
$self->{pbot}->{registry}->add_default('text', 'banlist', 'mute_mode_char', 'q');
my $data_dir = $self->{pbot}->{registry}->get_value('general', 'data_dir');
2021-07-24 04:22:25 +02:00
$self->{'ban-exemptions'} = PBot::Core::Storage::DualIndexHashObject->new(
pbot => $self->{pbot},
name => 'Ban exemptions',
filename => "$data_dir/ban-exemptions",
);
2021-07-24 04:22:25 +02:00
$self->{banlist} = PBot::Core::Storage::DualIndexHashObject->new(
pbot => $self->{pbot},
name => 'Ban List',
filename => "$data_dir/banlist",
save_queue_timeout => 15,
);
2021-07-24 04:22:25 +02:00
$self->{quietlist} = PBot::Core::Storage::DualIndexHashObject->new(
pbot => $self->{pbot},
name => 'Quiet List',
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->{quietlist}, $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char'));
$self->{ban_queue} = {};
$self->{unban_queue} = {};
$self->{pbot}->{event_queue}->enqueue(sub { $self->flush_unban_queue }, 30, 'Flush unban queue');
2011-02-13 06:07:02 +01:00
}
sub checkban {
my ($self, $channel, $mode, $mask) = @_;
$mask = $self->nick_to_banmask($mask);
my $data;
if ($mode eq 'b') {
$data = $self->{banlist}->get_data($channel, $mask);
} elsif ($mode eq $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char')) {
$data = $self->{quietlist}->get_data($channel, $mask);
}
if (not defined $data) {
return "$mask is not " . ($mode eq 'b' ? 'banned' : 'muted') . ".";
}
my $result = "$mask " . ($mode eq 'b' ? 'banned' : 'quieted') . " in $channel ";
if (defined $data->{timestamp}) {
my $date = strftime "%a %b %e %H:%M:%S %Y %Z", localtime $data->{timestamp};
my $ago = concise ago (time - $data->{timestamp});
$result .= "on $date ($ago) ";
}
$result .= "by $data->{owner} " if defined $data->{owner};
$result .= "for $data->{reason} " if defined $data->{reason};
if (exists $data->{timeout} and $data->{timeout} > 0) {
my $duration = concise duration($data->{timeout} - gettimeofday);
$result .= "($duration remaining)";
}
return $result;
}
sub is_ban_exempted {
my ($self, $channel, $hostmask) = @_;
return 1 if $self->{'ban-exemptions'}->exists(lc $channel, lc $hostmask);
return 0;
}
sub is_banned {
my ($self, $channel, $nick, $user, $host) = @_;
my $message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host);
my @nickserv_accounts = $self->{pbot}->{messagehistory}->{database}->get_nickserv_accounts($message_account);
push @nickserv_accounts, undef;
my $banned = undef;
foreach my $nickserv_account (@nickserv_accounts) {
my $baninfos = $self->get_baninfo($channel, "$nick!$user\@$host", $nickserv_account);
if (defined $baninfos) {
foreach my $baninfo (@$baninfos) {
my $u = $self->{pbot}->{users}->loggedin($baninfo->{channel}, "$nick!$user\@$host");
my $whitelisted = $self->{pbot}->{capabilities}->userhas($u, 'is-whitelisted');
if ($self->is_ban_exempted($baninfo->{channel}, $baninfo->{mask}) || $whitelisted) {
$self->{pbot}->{logger}->log("[BanList] is_banned: $nick!$user\@$host 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("[BanList] is_banned: $nick!$user\@$host $mode as $baninfo->{mask} in $baninfo->{channel} by $baninfo->{owner}\n");
$banned = $baninfo;
last;
}
}
}
}
}
return $banned;
}
sub has_ban_timeout {
my ($self, $channel, $mask, $mode) = @_;
$mode ||= 'b';
my $list = $mode eq 'b' ? $self->{banlist} : $self->{quietlist};
my $data = $list->get_data($channel, $mask);
if (defined $data && $data->{timeout} > 0) {
return 1;
} else {
return 0;
}
}
sub ban_user_timed {
my ($self, $channel, $mode, $mask, $length, $owner, $reason, $immediately) = @_;
$channel = lc $channel;
$mask = lc $mask;
$mask = $self->nick_to_banmask($mask);
$self->ban_user($channel, $mode, $mask, $immediately);
2020-02-15 23:38:32 +01:00
my $data = {
timeout => $length > 0 ? gettimeofday + $length : -1,
owner => $owner,
reason => $reason,
timestamp => time,
};
2020-02-15 23:38:32 +01:00
if ($mode eq 'b') {
$self->{banlist}->remove($channel, $mask, 'timeout');
$self->{banlist}->add($channel, $mask, $data);
} elsif ($mode eq $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char')) {
$self->{quietlist}->remove($channel, $mask, 'timeout');
$self->{quietlist}->add($channel, $mask, $data);
}
my $method = $mode eq 'b' ? 'unban' : 'unmute';
$self->{pbot}->{event_queue}->dequeue_event("$method $channel $mask");
if ($length > 0) {
$self->enqueue_unban($channel, $mode, $mask, $length);
}
}
sub ban_user {
my ($self, $channel, $mode, $mask, $immediately) = @_;
$mode ||= 'b';
$self->{pbot}->{logger}->log("Banning $channel +$mode $mask\n");
$self->add_to_ban_queue($channel, $mode, $mask);
if (not defined $immediately or $immediately != 0) {
$self->flush_ban_queue;
}
}
2020-02-15 23:38:32 +01:00
sub unban_user {
my ($self, $channel, $mode, $mask, $immediately) = @_;
$mask = lc $mask;
$channel = lc $channel;
$mode ||= 'b';
$self->{pbot}->{logger}->log("Unbanning $channel -$mode $mask\n");
$self->unmode_user($channel, $mode, $mask, $immediately);
}
2020-02-15 23:38:32 +01:00
sub unmode_user {
my ($self, $channel, $mode, $mask, $immediately) = @_;
$mask = lc $mask;
$channel = lc $channel;
$self->{pbot}->{logger}->log("Removing mode $mode from $mask in $channel\n");
my $bans = $self->get_bans($channel, $mask);
my %unbanned;
if (not defined $bans) {
push @$bans, { mask => $mask, type => $mode };
}
foreach my $ban (@$bans) {
next if $ban->{type} ne $mode;
2020-04-29 07:36:13 +02:00
next if exists $unbanned{$ban->{mask}};
$unbanned{$ban->{mask}} = 1;
$self->add_to_unban_queue($channel, $mode, $ban->{mask});
}
$self->flush_unban_queue if $immediately;
}
sub get_bans {
my ($self, $channel, $mask) = @_;
my $masks;
my ($message_account, $hostmask);
if ($mask !~ m/[!@]/) {
($message_account, $hostmask) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($mask);
$hostmask = $mask if not defined $message_account;
} else {
$message_account = $self->{pbot}->{messagehistory}->{database}->get_message_account_id($mask);
$hostmask = $mask;
}
if (defined $message_account) {
my $nickserv = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($message_account);
$masks = $self->get_baninfo($channel, $hostmask, $nickserv);
}
my %akas = $self->{pbot}->{messagehistory}->{database}->get_also_known_as($hostmask);
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{pbot}->{messagehistory}->{database}->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
my $nickserv = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($akas{$aka}->{id});
my $b = $self->get_baninfo($channel, $aka, $nickserv);
if (defined $b) {
push @$masks, @$b;
}
}
return $masks;
}
sub get_baninfo {
my ($self, $channel, $mask, $nickserv) = @_;
my ($bans, $ban_nickserv);
2013-07-28 12:31:12 +02:00
$nickserv = undef if not length $nickserv;
$nickserv = lc $nickserv if defined $nickserv;
2013-07-30 15:12:21 +02:00
if ($self->{pbot}->{registry}->get_value('banlist', 'debug')) {
my $ns = defined $nickserv ? $nickserv : "[undefined]";
$self->{pbot}->{logger}->log("[get-baninfo] Getting baninfo for $mask in $channel using nickserv $ns\n");
2020-02-15 23:38:32 +01:00
}
2013-08-03 19:26:49 +02:00
2020-02-15 23:38:32 +01:00
my ($nick, $user, $host) = $mask =~ m/([^!]+)!([^@]+)@(.*)/;
my @lists = (
[ 'b', $self->{banlist} ],
[ $self->{pbot}->{registry}->get_value('banlist', 'mute_mode_char'), $self->{quietlist} ],
);
2013-07-28 12:31:12 +02:00
foreach my $entry (@lists) {
my ($mode, $list) = @$entry;
foreach my $banmask ($list->get_keys($channel)) {
if ($banmask =~ m/^\$a:(.*)/) {
$ban_nickserv = lc $1;
} else {
$ban_nickserv = '';
}
my $banmask_regex = quotemeta $banmask;
$banmask_regex =~ s/\\\*/.*?/g;
$banmask_regex =~ s/\\\?/./g;
my $banned;
$banned = 1 if defined $nickserv and $nickserv eq $ban_nickserv;
$banned = 1 if $mask =~ m/^$banmask_regex$/i;
if ($banmask =~ m{\@gateway/web/irccloud.com} and $host =~ m{^gateway/web/irccloud.com}) {
my ($bannick, $banuser, $banhost) = $banmask =~ m/([^!]+)!([^@]+)@(.*)/;
$banned = $1 if lc $user eq lc $banuser;
2020-02-15 23:38:32 +01:00
}
2020-02-15 23:38:32 +01:00
if ($banned) {
my $data = $list->get_data($channel, $banmask);
my $baninfo = {
mask => $banmask,
channel => $channel,
owner => $data->{owner},
when => $data->{timestamp},
type => $mode,
reason => $data->{reason},
timeout => $data->{timeout},
};
2020-02-15 23:38:32 +01:00
push @$bans, $baninfo;
}
}
}
2020-02-15 23:38:32 +01:00
return $bans;
2011-02-13 06:07:02 +01:00
}
sub nick_to_banmask {
my ($self, $mask) = @_;
if ($mask !~ m/[!@\$]/) {
my ($message_account, $hostmask) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($mask);
if (defined $hostmask) {
my $nickserv = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($message_account);
if (defined $nickserv && length $nickserv) { $mask = '$a:' . $nickserv; }
else {
my ($nick, $user, $host) = $hostmask =~ m/([^!]+)!([^@]+)@(.*)/;
$mask = "*!$user\@" . $self->{pbot}->{antiflood}->address_to_mask($host);
}
} else {
$mask .= '!*@*';
}
}
# $a:account, etc, don't need wildcards appended
if ($mask =~ /^\$/) {
return $mask;
}
# ensure $mask is a complete hostmask by appending missing bits with wildcards
if ($mask !~ /!/) {
$mask .= '!*@*';
} elsif ($mask !~ /@/) {
$mask =~ s/\*?$/*@*/;
} else {
# TODO find out if/where this weird case happens and why...
$mask =~ s/\@$/@*/;
}
return $mask;
}
sub add_to_ban_queue {
my ($self, $channel, $mode, $mask) = @_;
if (not grep { $_ eq $mask } @{$self->{ban_queue}->{$channel}->{$mode}}) {
push @{$self->{ban_queue}->{$channel}->{$mode}}, $mask;
$self->{pbot}->{logger}->log("Added +$mode $mask for $channel to ban queue.\n");
}
}
sub add_to_unban_queue {
my ($self, $channel, $mode, $mask) = @_;
if (not grep { $_ eq $mask } @{$self->{unban_queue}->{$channel}->{$mode}}) {
push @{$self->{unban_queue}->{$channel}->{$mode}}, $mask;
$self->{pbot}->{logger}->log("Added -$mode $mask for $channel to unban queue.\n");
}
}
sub flush_ban_queue {
my $self = shift;
my $MAX_COMMANDS = 4;
my $commands = 0;
foreach my $channel (keys %{$self->{ban_queue}}) {
my $done = 0;
while (not $done) {
my ($list, $count, $modes);
$list = '';
$modes = '+';
$count = 0;
foreach my $mode (keys %{$self->{ban_queue}->{$channel}}) {
while (@{$self->{ban_queue}->{$channel}->{$mode}}) {
my $target = pop @{$self->{ban_queue}->{$channel}->{$mode}};
$list .= " $target";
$modes .= $mode;
last if ++$count >= $self->{pbot}->{isupport}->{MODES} // 1;
}
if (not @{$self->{ban_queue}->{$channel}->{$mode}}) {
delete $self->{ban_queue}->{$channel}->{$mode};
}
last if $count >= $self->{pbot}->{isupport}->{MODES} // 1;
}
if (not keys %{$self->{ban_queue}->{$channel}}) {
delete $self->{ban_queue}->{$channel};
$done = 1;
}
if ($count) {
$self->{pbot}->{chanops}->add_op_command($channel, "mode $channel $modes $list");
$self->{pbot}->{chanops}->gain_ops($channel);
return if ++$commands >= $MAX_COMMANDS;
}
}
}
}
sub flush_unban_queue {
my $self = shift;
my $MAX_COMMANDS = 4;
my $commands = 0;
foreach my $channel (keys %{$self->{unban_queue}}) {
my $done = 0;
while (not $done) {
my ($list, $count, $modes);
$list = '';
$modes = '-';
$count = 0;
foreach my $mode (keys %{$self->{unban_queue}->{$channel}}) {
while (@{$self->{unban_queue}->{$channel}->{$mode}}) {
my $target = pop @{$self->{unban_queue}->{$channel}->{$mode}};
$list .= " $target";
$modes .= $mode;
last if ++$count >= $self->{pbot}->{isupport}->{MODES} // 1;
}
if (not @{$self->{unban_queue}->{$channel}->{$mode}}) {
delete $self->{unban_queue}->{$channel}->{$mode};
}
last if $count >= $self->{pbot}->{isupport}->{MODES} // 1;
}
if (not keys %{$self->{unban_queue}->{$channel}}) {
delete $self->{unban_queue}->{$channel};
$done = 1;
}
if ($count) {
$self->{pbot}->{chanops}->add_op_command($channel, "mode $channel $modes $list");
$self->{pbot}->{chanops}->gain_ops($channel);
return if ++$commands >= $MAX_COMMANDS;
}
}
}
}
sub enqueue_unban {
my ($self, $channel, $mode, $hostmask, $interval) = @_;
my $method = $mode eq 'b' ? 'unban' : 'unmute';
$self->{pbot}->{event_queue}->enqueue_event(
sub {
$self->{pbot}->{event_queue}->update_interval("$method $channel $hostmask", 60 * 5, 1); # try again in 5 minutes
return if not $self->{pbot}->{joined_channels};
$self->unban_user($channel, $mode, $hostmask);
}, $interval, "$method $channel $hostmask", 1
);
}
sub enqueue_timeouts {
my ($self, $list, $mode) = @_;
my $now = time;
foreach my $channel ($list->get_keys) {
foreach my $mask ($list->get_keys($channel)) {
my $timeout = $list->get_data($channel, $mask, 'timeout');
2020-04-29 07:36:13 +02:00
next if defined $timeout and $timeout <= 0;
next if not defined $timeout;
my $interval = $timeout - $now;
$interval = 10 if $interval < 10;
$self->enqueue_unban($channel, $mode, $mask, $interval);
}
}
}
2011-02-13 06:07:02 +01:00
1;