pbot/PBot/MessageHistory.pm

378 lines
15 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;
2020-02-15 23:38:32 +01:00
use parent 'PBot::Class';
use warnings; use strict;
2019-07-11 03:40:53 +02:00
use feature 'unicode_strings';
use Getopt::Long qw(GetOptionsFromArray);
use Time::HiRes qw(gettimeofday tv_interval);
use Time::Duration;
use PBot::MessageHistory_SQLite;
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
$self->{filename} = $conf{filename} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/message_history.sqlite3';
2020-02-15 23:38:32 +01:00
$self->{database} = PBot::MessageHistory_SQLite->new(pbot => $self->{pbot}, filename => $self->{filename});
$self->{database}->begin();
$self->{database}->devalidate_all_channels();
2020-02-15 23:38:32 +01:00
$self->{MSG_CHAT} = 0; # PRIVMSG, ACTION
$self->{MSG_JOIN} = 1; # JOIN
$self->{MSG_DEPARTURE} = 2; # PART, QUIT, KICK
$self->{MSG_NICKCHANGE} = 3; # CHANGED NICK
2020-02-15 23:38:32 +01:00
$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->cmd_list_also_known_as(@_) }, "aka", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_recall_message(@_) }, "recall", 0);
$self->{pbot}->{commands}->register(sub { $self->cmd_rebuild_aliases(@_) }, "rebuildaliases", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_aka_link(@_) }, "akalink", 1);
$self->{pbot}->{commands}->register(sub { $self->cmd_aka_unlink(@_) }, "akaunlink", 1);
2020-02-15 23:38:32 +01:00
$self->{pbot}->{capabilities}->add('admin', 'can-akalink', 1);
$self->{pbot}->{capabilities}->add('admin', 'can-akaunlink', 1);
2020-02-15 23:38:32 +01:00
$self->{pbot}->{atexit}->register(sub { $self->{database}->end(); return; });
}
sub cmd_list_also_known_as {
my ($self, $context) = @_;
2020-02-15 23:38:32 +01: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 $context->{arguments}) { return $usage; }
2020-02-15 23:38:32 +01:00
my $getopt_error;
local $SIG{__WARN__} = sub {
$getopt_error = shift;
chomp $getopt_error;
};
Getopt::Long::Configure("bundling");
my ($show_hostmasks, $show_gecos, $show_nickserv, $show_id, $show_relationship, $show_weak, $dont_use_aliases_table);
my @opt_args = $self->{pbot}->{interpreter}->split_line($context->{arguments}, strip_quotes => 1);
GetOptionsFromArray(
\@opt_args,
2020-02-15 23:38:32 +01:00
'h' => \$show_hostmasks,
'n' => \$show_nickserv,
'r' => \$show_relationship,
'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 @opt_args > 1;
return "Missing argument -- $usage" if @opt_args != 1;
2020-02-15 23:38:32 +01:00
my %akas = $self->{database}->get_also_known_as($opt_args[0], $dont_use_aliases_table);
2020-02-15 23:38:32 +01:00
if (%akas) {
my $result = "$opt_args[0] also known as:\n";
2020-02-15 23:38:32 +01:00
my %nicks;
my $sep = "";
foreach my $aka (sort keys %akas) {
next if $aka =~ /^Guest\d+(?:!.*)?$/;
next if $akas{$aka}->{type} == $self->{database}->{alias_type}->{WEAK} && not $show_weak;
2020-02-15 23:38:32 +01:00
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};
$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}]";
}
$result .= " [WEAK]" if $akas{$aka}->{type} == $self->{database}->{alias_type}->{WEAK};
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 $opt_args[0].";
}
}
sub cmd_recall_message {
my ($self, $context) = @_;
if (not defined $context->{from}) {
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Command missing ~from parameter!\n");
return "";
}
my $usage = 'Usage: recall [nick [history [channel]]] [-c <channel>] [-t <text>] [-b <context before>] [-a <context after>] [-x <filter to nick>] [-n <count>] [-r raw mode] [+ ...]';
my $arguments = $context->{arguments};
if (not length $arguments) { return $usage; }
2020-02-15 23:38:32 +01:00
$arguments = lc $arguments;
2020-02-15 23:38:32 +01:00
my @recalls = split /\s\+\s/, $arguments;
2020-02-15 23:38:32 +01:00
my $getopt_error;
local $SIG{__WARN__} = sub {
$getopt_error = shift;
chomp $getopt_error;
};
2020-02-15 23:38:32 +01:00
my $recall_text = '';
Getopt::Long::Configure("bundling_override");
# global state
my ($recall_channel, $raw, $random);
2020-02-15 23:38:32 +01:00
foreach my $recall (@recalls) {
my ($recall_nick, $recall_history, $recall_before, $recall_after, $recall_context, $recall_count);
my @opt_args = $self->{pbot}->{interpreter}->split_line($recall, strip_quotes => 1);
GetOptionsFromArray(
\@opt_args,
2020-02-15 23:38:32 +01: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,
'raw|r' => \$raw,
'random' => \$random,
2020-02-15 23:38:32 +01:00
);
2020-02-15 23:38:32 +01:00
return "/say $getopt_error -- $usage" if defined $getopt_error;
2020-02-15 23:38:32 +01:00
my $channel_arg = 1 if defined $recall_channel;
my $history_arg = 1 if defined $recall_history;
$recall_nick = shift @opt_args if @opt_args;
$recall_history = shift @opt_args if @opt_args and not $history_arg;
$recall_channel = "@opt_args" if @opt_args and not $channel_arg;
2015-06-16 02:58:25 +02:00
2020-02-15 23:38:32 +01:00
$recall_count = 1 if (not defined $recall_count) || ($recall_count <= 0);
return "You may only select a count of up to 100 messages." if $recall_count > 100;
2015-06-16 02:58:25 +02:00
2020-02-15 23:38:32 +01:00
$recall_before = 0 if not defined $recall_before;
$recall_after = 0 if not defined $recall_after;
# imply -x if -n > 1 and no history and -x isn't already set to somebody
if ($recall_count > 1 and not defined $recall_history and not defined $recall_context) { $recall_context = $recall_nick; }
# make -n behave like -b if -n > 1 and no history is specified
if (not defined $recall_history and $recall_count > 1) {
2020-02-15 23:38:32 +01:00
$recall_before = $recall_count - 1;
$recall_count = 0;
}
if ($recall_before + $recall_after > 100) { return "You may only select up to 100 lines of surrounding context."; }
2015-06-16 02:58:25 +02:00
if ($recall_count > 1 and ($recall_before > 0 or $recall_after > 0)) { return "The `count` and `before/after` options cannot be used together."; }
2020-02-15 23:38:32 +01:00
# 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;
}
2020-02-15 23:38:32 +01:00
$recall_history = 1 if not defined $recall_history;
2020-02-15 23:38:32 +01:00
# 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;
}
2020-02-15 23:38:32 +01:00
# skip recall command if recalling self without arguments
$recall_history = $context->{nick} eq $recall_nick ? 2 : 1 if defined $recall_nick and not defined $recall_history;
2020-02-15 23:38:32 +01:00
# set history to most recent message if not specified
$recall_history = '1' if not defined $recall_history;
2020-02-15 23:38:32 +01:00
# set channel to current channel if not specified
$recall_channel = $context->{from} if not defined $recall_channel;
# yet another sanity check for people using it wrong
2020-02-15 23:38:32 +01:00
if ($recall_channel !~ m/^#/) {
$recall_history = "$recall_history $recall_channel";
$recall_channel = $context->{from};
2020-02-15 23:38:32 +01:00
}
2017-04-11 04:18:20 +02:00
2020-02-15 23:38:32 +01:00
if (not defined $recall_nick and defined $recall_context) { $recall_nick = $recall_context; }
2020-02-15 23:38:32 +01:00
my ($account, $found_nick);
2020-02-15 23:38:32 +01:00
if (defined $recall_nick) {
($account, $found_nick) = $self->{database}->find_message_account_by_nick($recall_nick);
2020-02-15 23:38:32 +01:00
if (not defined $account) { return "I don't know anybody named $recall_nick."; }
2020-02-15 23:38:32 +01:00
$found_nick =~ s/!.*$//;
}
2020-02-15 23:38:32 +01:00
my $message;
if ($random) {
$message = $self->{database}->get_random_message($account, $recall_channel);
} elsif ($recall_history =~ /^\d+$/) {
2020-02-15 23:38:32 +01:00
# integral history
if (defined $account) {
my $max_messages = $self->{database}->get_max_messages($account, $recall_channel);
if ($recall_history < 1 || $recall_history > $max_messages) {
if ($max_messages == 0) {
return "No messages for $recall_nick in $recall_channel yet.";
2020-02-15 23:38:32 +01:00
} else {
return "Please choose a history between 1 and $max_messages";
}
}
}
2020-02-15 23:38:32 +01:00
$recall_history--;
$message = $self->{database}->recall_message_by_count($account, $recall_channel, $recall_history, '(?:recall|mock|ftfy|fix|clapper)');
2020-02-15 23:38:32 +01:00
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|clapper)');
2020-02-15 23:38:32 +01:00
if (not defined $message) {
if (defined $account) { return "No message for nick $found_nick in channel $recall_channel containing \"$recall_history\""; }
else { return "No message in channel $recall_channel containing \"$recall_history\"."; }
}
}
2020-02-15 23:38:32 +01:00
my $context_account;
2020-02-15 23:38:32 +01:00
if (defined $recall_context) {
($context_account) = $self->{database}->find_message_account_by_nick($recall_context);
2020-02-15 23:38:32 +01:00
if (not defined $context_account) { return "I don't know anybody named $recall_context."; }
}
if ($context->{from} =~ /^#/ and ($recall_count > 5 or $recall_after > 5 or $recall_before > 5)) {
return "Please use `recall` from private message when recalling multiple messages. Just add \"-c $context->{from}\" to the command and /msg it to me.";
}
2020-02-15 23:38:32 +01:00
my $messages = $self->{database}->get_message_context($message, $recall_before, $recall_after, $recall_count, $recall_history, $context_account);
2020-02-15 23:38:32 +01:00
my $max_recall_time = $self->{pbot}->{registry}->get_value('messagehistory', 'max_recall_time');
2020-02-15 23:38:32 +01:00
foreach my $msg (@$messages) {
if ($max_recall_time && gettimeofday - $msg->{timestamp} > $max_recall_time && not $self->{pbot}->{users}->loggedin_admin($context->{from}, $context->{hostmask})) {
2020-02-15 23:38:32 +01:00
$max_recall_time = duration($max_recall_time);
$recall_text .= "Sorry, you can not recall messages older than $max_recall_time.";
return $recall_text;
}
2020-02-15 23:38:32 +01:00
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 .= $raw ? "$text\n" : "[$ago] $msg->{nick} $text\n";
2020-02-15 23:38:32 +01:00
} elsif ($text =~ s/^\/me\s+//) {
$recall_text .= $raw ? "$text\n" : "[$ago] * $msg->{nick} $text\n";
2020-02-15 23:38:32 +01:00
} else {
$recall_text .= $raw ? "$text\n" : "[$ago] <$msg->{nick}> $text\n";
2020-02-15 23:38:32 +01:00
}
}
}
2020-02-15 23:38:32 +01:00
return $recall_text;
}
sub cmd_rebuild_aliases {
my ($self, $context) = @_;
$self->{database}->rebuild_aliases_table;
}
sub cmd_aka_link {
my ($self, $context) = @_;
my ($id, $alias, $type) = split /\s+/, $context->{arguments};
$type = $self->{database}->{alias_type}->{STRONG} if not defined $type;
if (not $id or not $alias) { return "Usage: link <target id> <alias id> [type]"; }
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.";
} else {
return "Link failed.";
}
}
sub cmd_aka_unlink {
my ($self, $context) = @_;
my ($id, $alias) = split /\s+/, $context->{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."; }
else { return "Unlink failed."; }
}
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});
}
1;