pbot/PBot/ChanOps.pm

503 lines
14 KiB
Perl
Raw Normal View History

# File: ChanOps.pm
# Author: pragma_
#
# Purpose: Provides channel operator status tracking and commands.
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::ChanOps;
use warnings;
use strict;
2019-07-11 03:40:53 +02:00
use feature 'unicode_strings';
use PBot::ChanOpCommands;
use Time::HiRes qw(gettimeofday);
sub new {
if (ref($_[1]) eq 'HASH') {
Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference");
}
my ($class, %conf) = @_;
my $self = bless {}, $class;
$self->initialize(%conf);
return $self;
}
sub initialize {
my ($self, %conf) = @_;
$self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to ChanOps");
$self->{unban_timeout} = PBot::DualIndexHashObject->new(
pbot => $self->{pbot},
name => 'Unban Timeouts',
filename => $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/unban_timeouts'
);
$self->{unban_timeout}->load;
2015-05-27 19:45:43 +02:00
$self->{unmute_timeout} = PBot::DualIndexHashObject->new(
pbot => $self->{pbot},
name => 'Unmute Timeouts',
filename => $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/unmute_timeouts'
);
$self->{unmute_timeout}->load;
$self->{ban_queue} = {};
$self->{unban_queue} = {};
$self->{op_commands} = {};
$self->{is_opped} = {};
$self->{op_requested} = {};
$self->{commands} = PBot::ChanOpCommands->new(pbot => $self->{pbot});
$self->{pbot}->{registry}->add_default('text', 'general', 'deop_timeout', 300);
2015-05-27 19:45:43 +02:00
$self->{pbot}->{timer}->register(sub { $self->check_opped_timeouts }, 10);
$self->{pbot}->{timer}->register(sub { $self->check_unban_timeouts }, 10);
$self->{pbot}->{timer}->register(sub { $self->check_unmute_timeouts }, 10);
$self->{pbot}->{timer}->register(sub { $self->check_unban_queue }, 30);
}
sub can_gain_ops {
my ($self, $channel) = @_;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
return exists $self->{pbot}->{channels}->{channels}->{hash}->{$channel}
&& $self->{pbot}->{channels}->{channels}->{hash}->{$channel}->{chanop}
&& $self->{pbot}->{channels}->{channels}->{hash}->{$channel}->{enabled};
}
sub gain_ops {
my $self = shift;
my $channel = shift;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
return if exists $self->{op_requested}->{$channel};
return if not $self->can_gain_ops($channel);
my $op_nick = $self->{pbot}->{registry}->get_value($channel, 'op_nick') //
$self->{pbot}->{registry}->get_value('general', 'op_nick') // 'chanserv';
my $op_command = $self->{pbot}->{registry}->get_value($channel, 'op_command') //
$self->{pbot}->{registry}->get_value('general', 'op_command') // "op $channel";
$op_command =~ s/\$channel\b/$channel/g;
if (not exists $self->{is_opped}->{$channel}) {
$self->{pbot}->{conn}->privmsg($op_nick, $op_command);
$self->{op_requested}->{$channel} = scalar gettimeofday;
} else {
$self->perform_op_commands($channel);
}
}
sub lose_ops {
my $self = shift;
my $channel = shift;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
$self->{pbot}->{conn}->mode($channel, '-o ' . $self->{pbot}->{registry}->get_value('irc', 'botnick'));
}
sub add_op_command {
my ($self, $channel, $command) = @_;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
return if not $self->can_gain_ops($channel);
push @{ $self->{op_commands}->{$channel} }, $command;
}
sub perform_op_commands {
my $self = shift;
my $channel = shift;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
$self->{pbot}->{logger}->log("Performing op commands...\n");
while (my $command = shift @{ $self->{op_commands}->{$channel} }) {
if ($command =~ /^mode (.*?) (.*)/i) {
$self->{pbot}->{conn}->mode($1, $2);
2020-01-29 22:35:56 +01:00
$self->{pbot}->{logger}->log(" executing mode $1 $2\n");
} elsif ($command =~ /^kick (.*?) (.*?) (.*)/i) {
$self->{pbot}->{conn}->kick($1, $2, $3) unless $1 =~ /^\Q$botnick\E$/i;
$self->{pbot}->{logger}->log(" executing kick on $1 $2 $3\n");
2019-12-29 08:17:15 +01:00
} elsif ($command =~ /^sl (.*)/i) {
$self->{pbot}->{conn}->sl($1);
$self->{pbot}->{logger}->log(" executing sl $1\n");
}
}
$self->{pbot}->{logger}->log("Done.\n");
}
sub ban_user {
my $self = shift;
my ($mask, $channel, $immediately) = @_;
$self->add_to_ban_queue($channel, 'b', $mask);
if (not defined $immediately or $immediately != 0) {
$self->check_ban_queue;
}
}
sub get_bans {
my ($self, $mask, $channel) = @_;
my $masks;
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);
$masks = $self->{pbot}->{bantracker}->get_baninfo($hostmask, $channel, $nickserv);
}
my %akas = $self->{pbot}->{messagehistory}->{database}->get_also_known_as($mask);
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{pbot}->{messagehistory}->{database}->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
my $b = $self->{pbot}->{bantracker}->get_baninfo($aka, $channel);
if (defined $b) {
$masks = {} if not defined $masks;
push @$masks, @$b;
}
}
}
return $masks
}
sub unmode_user {
my ($self, $mask, $channel, $immediately, $mode) = @_;
$mask = lc $mask;
$channel = lc $channel;
$self->{pbot}->{logger}->log("Removing mode $mode from $mask in $channel\n");
my $bans = $self->get_bans($mask, $channel);
my %unbanned;
if (not defined $bans) {
my $baninfo = {};
$baninfo->{banmask} = $mask;
$baninfo->{type} = '+' . $mode;
push @$bans, $baninfo;
}
foreach my $baninfo (@$bans) {
next if $baninfo->{type} ne '+' . $mode;
next if exists $unbanned{$baninfo->{banmask}};
$unbanned{$baninfo->{banmask}} = 1;
$self->add_to_unban_queue($channel, $mode, $baninfo->{banmask});
}
$self->check_unban_queue if $immediately;
}
sub nick_to_banmask {
my ($self, $mask) = @_;
2016-10-29 19:47:08 +02:00
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 .= '!*@*';
}
}
return $mask;
}
sub unban_user {
my ($self, $mask, $channel, $immediately) = @_;
$mask = lc $mask;
$channel = lc $channel;
$self->{pbot}->{logger}->log("Unbanning $channel $mask\n");
$self->unmode_user($mask, $channel, $immediately, 'b');
}
sub ban_user_timed {
my $self = shift;
my ($owner, $reason, $mask, $channel, $length, $immediately) = @_;
$channel = lc $channel;
$mask = lc $mask;
$mask = $self->nick_to_banmask($mask);
$self->ban_user($mask, $channel, $immediately);
if ($length > 0) {
my $data = {
timeout => gettimeofday + $length
};
$data->{owner} = $owner if defined $owner;
$data->{reason} = $reason if defined $reason;
$self->{unban_timeout}->add($channel, $mask, $data);
} else {
$self->{unban_timeout}->remove($channel, $mask);
}
}
2015-05-27 19:45:43 +02:00
sub mute_user {
my $self = shift;
my ($mask, $channel, $immediately) = @_;
2015-05-27 19:45:43 +02:00
$self->add_to_ban_queue($channel, 'q', $mask);
if (not defined $immediately or $immediately != 0) {
$self->check_ban_queue;
}
2015-05-27 19:45:43 +02:00
}
sub unmute_user {
my ($self, $mask, $channel, $immediately) = @_;
2016-10-29 19:47:08 +02:00
$mask = lc $mask;
$channel = lc $channel;
2015-05-27 19:45:43 +02:00
$self->{pbot}->{logger}->log("Unmuting $channel $mask\n");
$self->unmode_user($mask, $channel, $immediately, 'q');
2015-05-27 19:45:43 +02:00
}
sub mute_user_timed {
my $self = shift;
my ($owner, $reason, $mask, $channel, $length, $immediately) = @_;
2015-05-27 19:45:43 +02:00
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
$mask = lc $mask;
$mask = $self->nick_to_banmask($mask);
$self->mute_user($mask, $channel, $immediately);
2015-05-27 19:45:43 +02:00
if ($length > 0) {
my $data = {
timeout => gettimeofday + $length
};
$data->{owner} = $owner if defined $owner;
$data->{reason} = $reason if defined $reason;
$self->{unmute_timeout}->add($channel, $mask, $data);
} else {
$self->{unmute_timeout}->remove($channel, $mask);
2015-05-27 19:45:43 +02:00
}
}
sub join_channel {
my ($self, $channels) = @_;
$self->{pbot}->{conn}->join($channels);
foreach my $channel (split /,/, $channels) {
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
$self->{pbot}->{event_dispatcher}->dispatch_event('pbot.join', { channel => $channel });
delete $self->{is_opped}->{$channel};
delete $self->{op_requested}->{$channel};
if (exists $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}
and $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}->{permop}) {
$self->gain_ops($channel);
}
$self->{pbot}->{conn}->mode($channel);
}
}
sub part_channel {
my ($self, $channel) = @_;
2016-10-29 19:47:08 +02:00
$channel = lc $channel;
$self->{pbot}->{event_dispatcher}->dispatch_event('pbot.part', { channel => $channel });
$self->{pbot}->{conn}->part($channel);
delete $self->{is_opped}->{$channel};
delete $self->{op_requested}->{$channel};
}
sub has_ban_timeout {
my ($self, $channel, $mask) = @_;
return exists $self->{unban_timeout}->{hash}->{lc $channel}->{lc $mask};
}
sub has_mute_timeout {
my ($self, $channel, $mask) = @_;
return exists $self->{unmute_timeout}->{hash}->{lc $channel}->{lc $mask};
}
sub add_to_ban_queue {
my ($self, $channel, $mode, $target) = @_;
push @{$self->{ban_queue}->{$channel}->{$mode}}, $target;
$self->{pbot}->{logger}->log("Added +$mode $target for $channel to ban queue.\n");
}
sub check_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}->{ircd}->{MODES};
}
if (not @{$self->{ban_queue}->{$channel}->{$mode}}) {
delete $self->{ban_queue}->{$channel}->{$mode};
}
last if $count >= $self->{pbot}->{ircd}->{MODES};
}
if (not keys %{ $self->{ban_queue}->{$channel} }) {
delete $self->{ban_queue}->{$channel};
$done = 1;
}
if ($count) {
$self->add_op_command($channel, "mode $channel $modes $list");
$self->gain_ops($channel);
return if ++$commands >= $MAX_COMMANDS;
}
}
}
}
sub add_to_unban_queue {
my ($self, $channel, $mode, $target) = @_;
push @{$self->{unban_queue}->{$channel}->{$mode}}, $target;
$self->{pbot}->{logger}->log("Added -$mode $target for $channel to unban queue.\n");
}
sub check_unban_queue {
my $self = shift;
2017-08-03 22:30:42 +02:00
my $MAX_COMMANDS = 4;
my $commands = 0;
foreach my $channel (keys %{$self->{unban_queue}}) {
2017-08-03 22:30:42 +02:00
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}->{ircd}->{MODES};
}
if (not @{$self->{unban_queue}->{$channel}->{$mode}}) {
delete $self->{unban_queue}->{$channel}->{$mode};
}
last if $count >= $self->{pbot}->{ircd}->{MODES};
}
2017-08-09 10:22:13 +02:00
if (not keys %{ $self->{unban_queue}->{$channel} }) {
2017-08-03 22:30:42 +02:00
delete $self->{unban_queue}->{$channel};
$done = 1;
}
2017-08-03 22:30:42 +02:00
if ($count) {
$self->add_op_command($channel, "mode $channel $modes $list");
$self->gain_ops($channel);
2017-08-03 22:30:42 +02:00
return if ++$commands >= $MAX_COMMANDS;
}
}
}
}
sub check_unban_timeouts {
my $self = shift;
return if not $self->{pbot}->{joined_channels};
my $now = gettimeofday();
foreach my $channel (keys %{ $self->{unban_timeout}->{hash} }) {
foreach my $mask (keys %{ $self->{unban_timeout}->{hash}->{$channel} }) {
next if $mask eq '_name';
if ($self->{unban_timeout}->{hash}->{$channel}->{$mask}->{timeout} < $now) {
$self->{unban_timeout}->{hash}->{$channel}->{$mask}->{timeout} = $now + 7200;
$self->unban_user($mask, $channel);
}
}
}
}
2015-05-27 19:45:43 +02:00
sub check_unmute_timeouts {
my $self = shift;
return if not $self->{pbot}->{joined_channels};
my $now = gettimeofday();
foreach my $channel (keys %{ $self->{unmute_timeout}->{hash} }) {
foreach my $mask (keys %{ $self->{unmute_timeout}->{hash}->{$channel} }) {
next if $mask eq '_name';
if ($self->{unmute_timeout}->{hash}->{$channel}->{$mask}->{timeout} < $now) {
$self->{unmute_timeout}->{hash}->{$channel}->{$mask}->{timeout} = $now + 7200;
2015-05-27 19:45:43 +02:00
$self->unmute_user($mask, $channel);
}
}
}
}
sub check_opped_timeouts {
my $self = shift;
my $now = gettimeofday();
foreach my $channel (keys %{ $self->{is_opped} }) {
if ($self->{is_opped}->{$channel}{timeout} < $now) {
unless (exists $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}
and $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}->{permop}) {
$self->lose_ops($channel);
}
}
}
foreach my $channel (keys %{ $self->{op_requested} }) {
if ($now - $self->{op_requested}->{$channel} > 60 * 5) {
if (exists $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}
and $self->{pbot}->{channels}->{channels}->{hash}->{lc $channel}->{enabled}) {
$self->{pbot}->{logger}->log("5 minutes since OP request for $channel and no OP yet; trying again ...\n");
delete $self->{op_requested}->{$channel};
$self->gain_ops($channel);
} else {
$self->{pbot}->{logger}->log("Disregarding OP request for $channel (channel is disabled)\n");
delete $self->{op_requested}->{$channel};
}
}
}
}
1;