2014-05-13 12:15:52 +02:00
# File: MessageHistory.pm
# Author: pragma_
#
# Purpose: Keeps track of who has said what and when, as well as their
# nickserv accounts and alter-hostmasks.
#
# Used in conjunction with AntiFlood and Quotegrabs for kick/ban on
# flood/ban-evasion and grabbing quotes, respectively.
package PBot::MessageHistory ;
use warnings ;
use strict ;
2014-05-14 23:23:59 +02:00
use Getopt::Long qw( GetOptionsFromString ) ;
2014-05-13 12:15:52 +02:00
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__ ) ;
2014-05-17 22:08:19 +02:00
$ self - > { filename } = delete $ conf { filename } // $ self - > { pbot } - > { registry } - > get_value ( 'general' , 'data_dir' ) . '/message_history.sqlite3' ;
2014-05-13 12:15:52 +02:00
$ self - > { database } = PBot::MessageHistory_SQLite - > new ( pbot = > $ self - > { pbot } , filename = > $ self - > { filename } ) ;
$ self - > { database } - > begin ( ) ;
$ self - > { database } - > devalidate_all_channels ( ) ;
2014-05-16 02:48:46 +02: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
2014-05-13 12:15:52 +02:00
2014-05-18 22:09:05 +02:00
$ self - > { pbot } - > { registry } - > add_default ( 'text' , 'messagehistory' , 'max_messages' , $ conf { max_messages } // 32 ) ;
2015-05-12 06:27:22 +02:00
$ 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 ) ;
2014-05-17 00:11:31 +02:00
$ self - > { pbot } - > { atexit } - > register ( sub { $ self - > { database } - > end ( ) ; return ; } ) ;
2014-05-13 12:15:52 +02:00
}
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 } ) ;
}
2015-05-12 06:27:22 +02:00
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 ) = @ _ ;
2015-07-15 09:18:57 +02:00
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 ) {
2015-07-15 09:18:57 +02:00
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." ;
}
2015-07-15 09:18:57 +02:00
if ( $ self - > { database } - > link_alias ( $ id , $ alias , $ type ) ) {
2015-07-29 02:49:18 +02:00
return "$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 "$source unlinked from $target." ;
} else {
return "Unlink failed." ;
}
}
2014-07-11 14:57:18 +02:00
sub list_also_known_as {
my ( $ self , $ from , $ nick , $ user , $ host , $ arguments ) = @ _ ;
2015-07-15 09:18:57 +02:00
my $ usage = "Usage: aka [-h] [-i] [-n] [-r] [-w] <nick>; -h show hostmasks; -i show ids; -n show nickserv accounts; -r show relationships; -w show weak links" ;
2014-07-13 09:06:04 +02:00
2014-07-11 14:57:18 +02:00
if ( not length $ arguments ) {
2014-07-13 09:06:04 +02:00
return $ usage ;
2014-07-11 14:57:18 +02:00
}
2014-07-13 09:06:04 +02:00
my $ getopt_error ;
local $ SIG { __WARN__ } = sub {
$ getopt_error = shift ;
chomp $ getopt_error ;
} ;
2015-07-15 09:18:57 +02:00
my ( $ show_hostmasks , $ show_nickserv , $ show_id , $ show_relationship , $ show_weak , $ dont_use_aliases_table ) ;
2014-07-13 09:06:04 +02:00
my ( $ ret , $ args ) = GetOptionsFromString ( $ arguments ,
2015-05-12 06:27:22 +02:00
'h' = > \ $ show_hostmasks ,
'n' = > \ $ show_nickserv ,
2015-05-13 06:46:40 +02:00
'r' = > \ $ show_relationship ,
2015-07-15 09:18:57 +02:00
'w' = > \ $ show_weak ,
2015-05-12 06:27:22 +02:00
'nt' = > \ $ dont_use_aliases_table ,
'i' = > \ $ show_id ) ;
2014-07-13 09:06:04 +02:00
return "$getopt_error -- $usage" if defined $ getopt_error ;
return "Too many arguments -- $usage" if @$ args > 1 ;
return "Missing argument -- $usage" if @$ args != 1 ;
2015-05-12 06:27:22 +02:00
my % akas = $ self - > { database } - > get_also_known_as ( @$ args [ 0 ] , $ dont_use_aliases_table ) ;
if ( % akas ) {
2014-08-11 09:33:05 +02:00
my $ result = "@$args[0] also known as:\n" ;
2014-07-11 14:57:18 +02:00
2015-05-12 06:27:22 +02:00
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 ;
2015-05-12 06:27:22 +02:00
2014-07-13 09:06:04 +02:00
if ( not $ show_hostmasks ) {
2015-05-12 06:27:22 +02:00
my ( $ nick ) = $ aka =~ m/([^!]+)/ ;
2015-06-06 07:27:59 +02:00
next if exists $ nicks { $ nick } ;
2015-05-12 06:27:22 +02:00
$ nicks { $ nick } - > { id } = $ akas { $ aka } - > { id } ;
$ result . = "$sep$nick" ;
2014-07-13 09:06:04 +02:00
} else {
2015-05-12 06:27:22 +02:00
$ result . = "$sep$aka" ;
2014-07-13 09:06:04 +02:00
}
2015-05-12 06:27:22 +02:00
$ result . = " ($akas{$aka}->{nickserv})" if $ show_nickserv and exists $ akas { $ aka } - > { nickserv } ;
2015-05-13 06:46:40 +02:00
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 } ;
2015-07-15 09:18:57 +02:00
2015-05-13 06:46:40 +02:00
if ( $ show_hostmasks or $ show_nickserv or $ show_id or $ show_relationship ) {
2014-07-13 09:06:04 +02:00
$ sep = ",\n" ;
} else {
$ sep = ", " ;
}
2014-07-11 14:57:18 +02:00
}
return $ result ;
} else {
2014-07-13 09:06:04 +02:00
return "I don't know anybody named @$args[0]." ;
2014-07-11 14:57:18 +02:00
}
}
2014-05-13 12:15:52 +02:00
sub recall_message {
my ( $ self , $ from , $ nick , $ user , $ host , $ arguments ) = @ _ ;
if ( not defined $ from ) {
2014-05-18 22:09:05 +02:00
$ self - > { pbot } - > { logger } - > log ( "Command missing ~from parameter!\n" ) ;
2014-05-13 12:15:52 +02:00
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>] [+ ...]' ;
2014-05-14 23:23:59 +02:00
2014-05-13 12:15:52 +02:00
if ( not defined $ arguments or not length $ arguments ) {
2014-05-14 23:23:59 +02:00
return $ usage ;
2014-05-13 12:15:52 +02:00
}
$ arguments = lc $ arguments ;
my @ recalls = split /\s\+\s/ , $ arguments ;
2014-05-14 23:23:59 +02:00
my $ getopt_error ;
local $ SIG { __WARN__ } = sub {
$ getopt_error = shift ;
chomp $ getopt_error ;
} ;
my $ recall_text ;
2014-05-13 12:15:52 +02:00
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 ) ;
2014-05-14 23:23:59 +02:00
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 ) ;
2014-05-14 23:23:59 +02:00
return "$getopt_error -- $usage" if defined $ getopt_error ;
my $ channel_arg = 1 if defined $ recall_channel ;
my $ history_arg = 1 if defined $ recall_history ;
2014-05-13 12:15:52 +02:00
2014-05-14 23:23:59 +02:00
$ recall_nick = shift @$ args ;
$ recall_history = shift @$ args if not defined $ recall_history ;
$ recall_channel = shift @$ args if not defined $ recall_channel ;
2015-06-16 02:58:25 +02:00
$ recall_count = 1 if $ recall_count <= 0 ;
return "You may only select a count of up to 50 messages." if $ recall_count > 50 ;
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." ;
}
2014-05-13 12:15:52 +02:00
2014-05-14 23:23:59 +02: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 ;
}
2014-05-13 12:15:52 +02:00
2014-05-14 23:23:59 +02: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 ;
}
# 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 ;
2015-01-23 20:44:38 +01:00
if ( not defined $ recall_nick and defined $ recall_context ) {
$ recall_nick = $ recall_context ;
}
2014-05-14 23:23:59 +02:00
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." ;
}
2015-05-12 06:27:22 +02:00
$ found_nick =~ s/!.*$// ;
2014-05-13 12:15:52 +02:00
}
my $ message ;
if ( $ recall_history =~ /^\d+$/ ) {
# integral history
2014-05-14 23:23:59 +02:00
if ( defined $ account ) {
my $ max_messages = $ self - > { database } - > get_max_messages ( $ account , $ recall_channel ) ;
if ( $ recall_history < 1 || $ recall_history > $ max_messages ) {
return "Please choose a history between 1 and $max_messages" ;
}
2014-05-13 12:15:52 +02:00
}
$ recall_history - - ;
2014-05-14 23:23:59 +02:00
$ message = $ self - > { database } - > recall_message_by_count ( $ account , $ recall_channel , $ recall_history , 'recall' ) ;
2014-05-13 12:15:52 +02:00
2014-05-14 23:23:59 +02:00
if ( not defined $ message ) {
return "No message found at index $recall_history in channel $recall_channel." ;
}
2014-05-13 12:15:52 +02:00
} else {
# regex history
2014-05-14 23:23:59 +02:00
$ message = $ self - > { database } - > recall_message_by_text ( $ account , $ recall_channel , $ recall_history , 'recall' ) ;
2014-05-13 12:15:52 +02:00
if ( not defined $ message ) {
2014-05-14 23:23:59 +02:00
if ( defined $ account ) {
return "No such message for nick $found_nick in channel $recall_channel containing text '$recall_history'" ;
} else {
return "No such message in channel $recall_channel containing text '$recall_history'" ;
}
2014-05-13 12:15:52 +02:00
}
}
2015-01-23 20:44:38 +01:00
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 ) ;
2014-05-13 12:15:52 +02:00
2015-01-23 16:36:39 +01:00
foreach my $ msg ( @$ messages ) {
$ self - > { pbot } - > { logger } - > log ( "$nick ($from) recalled <$msg->{nick}/$msg->{channel}> $msg->{msg}\n" ) ;
2014-05-13 12:15:52 +02:00
2015-01-23 16:36:39 +01:00
my $ text = $ msg - > { msg } ;
my $ ago = ago ( gettimeofday - $ msg - > { timestamp } ) ;
if ( not defined $ recall_text ) {
if ( $ text =~ s/^\/me\s+// or $ text =~ m/^KICKED / ) {
$ recall_text = "[$ago] * $msg->{nick} $text\n" ;
} else {
$ recall_text = "[$ago] <$msg->{nick}> $text\n" ;
}
2014-05-13 12:15:52 +02:00
} else {
2015-01-23 16:36:39 +01:00
if ( $ text =~ s/^\/me\s+// or $ text =~ m/^KICKED / ) {
$ recall_text . = "[$ago] * $msg->{nick} $text\n" ;
} else {
$ recall_text . = "[$ago] <$msg->{nick}> $text\n" ;
}
2014-05-13 12:15:52 +02:00
}
}
}
return $ recall_text ;
}
1 ;