3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-12-24 11:42:35 +01:00
pbot/PBot/MessageHistory.pm

427 lines
14 KiB
Perl
Raw Normal View History

# File: MessageHistory.pm
# Author: pragma_
#
# Purpose: Keeps track of who has said what and when, as well as their
2019-06-26 18:34:19 +02:00
# nickserv accounts and alter-hostmasks.
#
# Used in conjunction with AntiFlood and Quotegrabs for kick/ban on
# flood/ban-evasion and grabbing quotes, respectively.
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::MessageHistory;
use warnings;
use strict;
2019-07-11 03:40:53 +02:00
use feature 'unicode_strings';
use Getopt::Long qw(GetOptionsFromString);
use Time::HiRes qw(gettimeofday tv_interval);
use Time::Duration;
use Carp ();
use PBot::MessageHistory_SQLite;
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 " . __FILE__);
$self->{filename} = delete $conf{filename} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/message_history.sqlite3';
$self->{database} = PBot::MessageHistory_SQLite->new(pbot => $self->{pbot}, filename => $self->{filename});
$self->{database}->begin();
$self->{database}->devalidate_all_channels();
$self->{MSG_CHAT} = 0; # PRIVMSG, ACTION
$self->{MSG_JOIN} = 1; # JOIN
$self->{MSG_DEPARTURE} = 2; # PART, QUIT, KICK
$self->{MSG_NICKCHANGE} = 3; # CHANGED NICK
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'max_recall_time', $conf{max_recall_time} // 0);
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'max_messages', 32);
$self->{pbot}->{commands}->register(sub { $self->recall_message(@_) }, "recall", 0);
$self->{pbot}->{commands}->register(sub { $self->list_also_known_as(@_) }, "aka", 0);
$self->{pbot}->{commands}->register(sub { $self->rebuild_aliases(@_) }, "rebuildaliases", 90);
2015-05-12 21:59:45 +02:00
$self->{pbot}->{commands}->register(sub { $self->aka_link(@_) }, "akalink", 60);
$self->{pbot}->{commands}->register(sub { $self->aka_unlink(@_) }, "akaunlink", 60);
$self->{pbot}->{atexit}->register(sub { $self->{database}->end(); return; });
}
sub get_message_account {
my ($self, $nick, $user, $host) = @_;
return $self->{database}->get_message_account($nick, $user, $host);
}
sub add_message {
my ($self, $account, $mask, $channel, $text, $mode) = @_;
$self->{database}->add_message($account, $mask, $channel, { timestamp => scalar gettimeofday, msg => $text, mode => $mode });
}
sub rebuild_aliases {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
$self->{database}->rebuild_aliases_table;
}
2015-05-12 21:59:45 +02:00
sub aka_link {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
my ($id, $alias, $type) = split /\s+/, $arguments;
2015-07-29 02:49:18 +02:00
$type = $self->{database}->{alias_type}->{STRONG} if not defined $type;
2015-05-12 21:59:45 +02:00
if (not $id or not $alias) {
return "Usage: link <target id> <alias id> [type]";
2015-05-12 21:59:45 +02:00
}
my $source = $self->{database}->find_most_recent_hostmask($id);
my $target = $self->{database}->find_most_recent_hostmask($alias);
if (not $source) {
return "No such id $id found.";
}
if (not $target) {
return "No such id $alias found.";
}
if ($self->{database}->link_alias($id, $alias, $type)) {
return "/say $source " . ($type == $self->{database}->{alias_type}->{WEAK} ? "weakly" : "strongly") . " linked to $target.";
2015-05-12 21:59:45 +02:00
} else {
return "Link failed.";
}
}
sub aka_unlink {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
my ($id, $alias) = split /\s+/, $arguments;
if (not $id or not $alias) {
return "Usage: unlink <target id> <alias id>";
}
my $source = $self->{database}->find_most_recent_hostmask($id);
my $target = $self->{database}->find_most_recent_hostmask($alias);
if (not $source) {
return "No such id $id found.";
}
if (not $target) {
return "No such id $alias found.";
}
if ($self->{database}->unlink_alias($id, $alias)) {
return "/say $source unlinked from $target.";
2015-05-12 21:59:45 +02:00
} else {
return "Unlink failed.";
}
}
sub list_also_known_as {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
2016-09-22 17:25:50 +02:00
my $usage = "Usage: aka [-hingr] <nick>; -h show hostmasks; -i show ids; -n show nickserv accounts; -g show gecos, -r show relationships";
if (not length $arguments) {
return $usage;
}
my $getopt_error;
local $SIG{__WARN__} = sub {
$getopt_error = shift;
chomp $getopt_error;
};
2017-05-21 08:43:34 +02:00
Getopt::Long::Configure ("bundling");
2016-02-10 12:42:42 +01:00
my ($show_hostmasks, $show_gecos, $show_nickserv, $show_id, $show_relationship, $show_weak, $dont_use_aliases_table);
my ($ret, $args) = GetOptionsFromString($arguments,
'h' => \$show_hostmasks,
'n' => \$show_nickserv,
'r' => \$show_relationship,
2016-02-10 12:42:42 +01:00
'g' => \$show_gecos,
'w' => \$show_weak,
'nt' => \$dont_use_aliases_table,
'i' => \$show_id);
return "/say $getopt_error -- $usage" if defined $getopt_error;
return "Too many arguments -- $usage" if @$args > 1;
return "Missing argument -- $usage" if @$args != 1;
my %akas = $self->{database}->get_also_known_as(@$args[0], $dont_use_aliases_table);
if (%akas) {
my $result = "@$args[0] also known as:\n";
my %nicks;
my $sep = "";
foreach my $aka (sort keys %akas) {
next if $aka =~ /^Guest\d+(?:!.*)?$/;
2015-07-29 02:49:18 +02:00
next if $akas{$aka}->{type} == $self->{database}->{alias_type}->{WEAK} && not $show_weak;
if (not $show_hostmasks) {
my ($nick) = $aka =~ m/([^!]+)/;
next if exists $nicks{$nick};
$nicks{$nick}->{id} = $akas{$aka}->{id};
$result .= "$sep$nick";
} else {
$result .= "$sep$aka";
}
$result .= "?" if $akas{$aka}->{nickchange} == 1;
$result .= " ($akas{$aka}->{nickserv})" if $show_nickserv and exists $akas{$aka}->{nickserv};
2016-02-10 12:42:42 +01:00
$result .= " {$akas{$aka}->{gecos}}" if $show_gecos and exists $akas{$aka}->{gecos};
if ($show_relationship) {
if ($akas{$aka}->{id} == $akas{$aka}->{alias}) {
$result .= " [$akas{$aka}->{id}]";
} else {
$result .= " [$akas{$aka}->{id} -> $akas{$aka}->{alias}]";
}
} elsif ($show_id) {
$result .= " [$akas{$aka}->{id}]";
}
2015-07-29 02:49:18 +02:00
$result .= " [WEAK]" if $akas{$aka}->{type} == $self->{database}->{alias_type}->{WEAK};
2016-02-10 12:42:42 +01:00
if ($show_hostmasks or $show_nickserv or $show_gecos or $show_id or $show_relationship) {
$sep = ",\n";
} else {
$sep = ", ";
}
}
return $result;
} else {
return "I don't know anybody named @$args[0].";
}
}
sub recall_message {
my ($self, $from, $nick, $user, $host, $arguments) = @_;
if (not defined $from) {
$self->{pbot}->{logger}->log("Command missing ~from parameter!\n");
return "";
}
2015-06-16 02:58:25 +02:00
my $usage = 'Usage: recall [nick [history [channel]]] [-c,channel <channel>] [-t,text,h,history <history>] [-b,before <context before>] [-a,after <context after>] [-x,context <nick>] [-n,count <count>] [+ ...]';
if (not defined $arguments or not length $arguments) {
2019-06-26 18:34:19 +02:00
return $usage;
}
$arguments = lc $arguments;
my @recalls = split /\s\+\s/, $arguments;
my $getopt_error;
local $SIG{__WARN__} = sub {
$getopt_error = shift;
chomp $getopt_error;
};
my $recall_text = '';
2017-08-09 06:18:29 +02:00
Getopt::Long::Configure ("bundling");
foreach my $recall (@recalls) {
2015-06-16 02:58:25 +02:00
my ($recall_nick, $recall_history, $recall_channel, $recall_before, $recall_after, $recall_context, $recall_count);
my ($ret, $args) = GetOptionsFromString($recall,
2015-06-16 02:58:25 +02:00
'channel|c:s' => \$recall_channel,
'text|t|history|h:s' => \$recall_history,
'before|b:i' => \$recall_before,
'after|a:i' => \$recall_after,
'count|n:i' => \$recall_count,
'context|x:s' => \$recall_context);
return "/say $getopt_error -- $usage" if defined $getopt_error;
my $channel_arg = 1 if defined $recall_channel;
my $history_arg = 1 if defined $recall_history;
$recall_nick = shift @$args if @$args;
$recall_history = shift @$args if @$args and not defined $recall_history;
$recall_channel = "@$args" if @$args and not defined $recall_channel;
2015-06-16 02:58:25 +02:00
$recall_count = 1 if (not defined $recall_count) || ($recall_count <= 0);
2015-06-16 02:58:25 +02:00
return "You may only select a count of up to 50 messages." if $recall_count > 50;
$recall_before = 0 if not defined $recall_before;
$recall_after = 0 if not defined $recall_after;
# imply -x if -n > 1 and no history
if ($recall_count > 1 and not defined $recall_history) {
$recall_context = $recall_nick;
}
# make -n behave like -b if -n > 1 and nick is context
if ((defined $recall_context or not defined $recall_history) and $recall_count > 1) {
$recall_before = $recall_count - 1;
$recall_count = 0;
}
2015-06-16 02:58:25 +02:00
if ($recall_before + $recall_after > 200) {
return "You may only select up to 200 lines of surrounding context.";
}
if ($recall_count > 1 and ($recall_before > 0 or $recall_after > 0)) {
return "The `count` and `context before/after` options cannot be used together.";
}
# swap nick and channel if recall nick looks like channel and channel wasn't specified
if (not $channel_arg and $recall_nick =~ m/^#/) {
my $temp = $recall_nick;
$recall_nick = $recall_channel;
$recall_channel = $temp;
}
$recall_history = 1 if not defined $recall_history;
# swap history and channel if history looks like a channel and neither history or channel were specified
if (not $channel_arg and not $history_arg and $recall_history =~ m/^#/) {
my $temp = $recall_history;
$recall_history = $recall_channel;
$recall_channel = $temp;
}
# skip recall command if recalling self without arguments
$recall_history = $nick eq $recall_nick ? 2 : 1 if defined $recall_nick and not defined $recall_history;
# set history to most recent message if not specified
$recall_history = '1' if not defined $recall_history;
# set channel to current channel if not specified
$recall_channel = $from if not defined $recall_channel;
2017-04-11 04:18:20 +02:00
# another sanity check for people using it wrong
if ($recall_channel !~ m/^#/) {
$recall_history = "$recall_history $recall_channel";
2017-04-11 04:18:20 +02:00
$recall_channel = $from;
}
if (not defined $recall_nick and defined $recall_context) {
$recall_nick = $recall_context;
}
my ($account, $found_nick);
if (defined $recall_nick) {
($account, $found_nick) = $self->{database}->find_message_account_by_nick($recall_nick);
if (not defined $account) {
return "I don't know anybody named $recall_nick.";
}
$found_nick =~ s/!.*$//;
}
my $message;
if ($recall_history =~ /^\d+$/) {
# integral history
if (defined $account) {
my $max_messages = $self->{database}->get_max_messages($account, $recall_channel);
2015-09-19 02:45:25 +02:00
if ($recall_history < 1 || $recall_history > $max_messages) {
if ($max_messages == 0) {
my @channels = $self->{database}->get_channels($account);
my $result = "No messages for $recall_nick in $recall_channel; I have messages for them in ";
my $comma = '';
my $count = 0;
foreach my $channel (sort @channels) {
next if $channel !~ /^#/;
$result .= "$comma$channel";
$comma = ', ';
$count++;
}
if ($count == 0) {
return "I have no messages for $recall_nick.";
} else {
return "/say $result.";
}
2015-09-19 02:45:25 +02:00
} else {
return "Please choose a history between 1 and $max_messages";
}
}
}
$recall_history--;
$message = $self->{database}->recall_message_by_count($account, $recall_channel, $recall_history, '(?:recall|mock|ftfy|fix)');
if (not defined $message) {
return "No message found at index $recall_history in channel $recall_channel.";
}
} else {
# regex history
$message = $self->{database}->recall_message_by_text($account, $recall_channel, $recall_history, '(?:recall|mock|ftfy|fix)');
if (not defined $message) {
if (defined $account) {
2017-04-11 04:18:20 +02:00
return "No message for nick $found_nick in channel $recall_channel containing \"$recall_history\"";
} else {
2017-04-11 04:18:20 +02:00
return "No message in channel $recall_channel containing \"$recall_history\".";
}
}
}
my $context_account;
if (defined $recall_context) {
($context_account) = $self->{database}->find_message_account_by_nick($recall_context);
if (not defined $context_account) {
return "I don't know anybody named $recall_context.";
}
}
2015-06-16 02:58:25 +02:00
my $messages = $self->{database}->get_message_context($message, $recall_before, $recall_after, $recall_count, $recall_history, $context_account);
my $max_recall_time = $self->{pbot}->{registry}->get_value('messagehistory', 'max_recall_time');
foreach my $msg (@$messages) {
$self->{pbot}->{logger}->log("$nick ($from) recalled <$msg->{nick}/$msg->{channel}> $msg->{msg}\n");
if ($max_recall_time && gettimeofday - $msg->{timestamp} > $max_recall_time && not $self->{pbot}->{admins}->loggedin($from, "$nick!$user\@$host")) {
$max_recall_time = duration($max_recall_time);
$recall_text .= "Sorry, you can not recall messages older than $max_recall_time.";
return $recall_text;
}
my $text = $msg->{msg};
my $ago = concise ago(gettimeofday - $msg->{timestamp});
if ($text =~ s/^(NICKCHANGE)\b/changed nick to/ or
$text =~ s/^(KICKED|QUIT)\b/lc "$1"/e or
$text =~ s/^MODE ([^ ]+) (.*)/set mode $1 on $2/ or
$text =~ s/^(JOIN|PART)\b/lc "$1ed"/e) {
$text =~ s/^(quit) (.*)/$1 ($2)/; # fix ugly "[nick] quit Quit: Leaving."
$recall_text .= "[$ago] $msg->{nick} $text\n";
} elsif ($text =~ s/^\/me\s+//) {
$recall_text .= "[$ago] * $msg->{nick} $text\n";
} else {
$recall_text .= "[$ago] <$msg->{nick}> $text\n";
}
}
}
return $recall_text;
}
1;