mirror of
https://github.com/pragma-/pbot.git
synced 2024-11-26 22:09:26 +01:00
Convert message history to use SQLite database instead of Perl hashtable
Added MessageHistory.pm and MessageHistory_SQLite.pm. May eventually port and add MessageHistory_Hashtable.pm as was done with Quotegrabs, but this is not particularly high on the todo list. Antiflood.pm has been updated to use the new MessageHistory API. The `recall` command has been moved from Quotegrabs into MessageHistory. It also now has the ability to ignore messages containing the recall command itself, for improved usability. Likewise, the `grab` command will now ignore previous `grab` commands when grabbing by regex in order to prevent accidentally grabbing previous grab attempts. The `join` and `part` commands have been improved to accept multiple channels, and `part` will use the current channel if none is provided.
This commit is contained in:
parent
ae1842e3db
commit
54ac8ec0ef
@ -1,12 +1,11 @@
|
||||
# File: AntiFlood.pm
|
||||
# Author: pragma_
|
||||
#
|
||||
# Purpose: Keeps track of who has said what and when. Used in
|
||||
# conjunction with ChanOps and Quotegrabs for kick/ban on flood
|
||||
# and grabbing quotes, respectively.
|
||||
# Purpose: Tracks message and nickserv statistics to enforce anti-flooding and
|
||||
# ban-evasion detection.
|
||||
#
|
||||
# We should take out the message-tracking stuff and put it in its own
|
||||
# MessageTracker class.
|
||||
# The nickserv/ban-evasion stuff probably ought to be in BanTracker or some
|
||||
# such suitable class.
|
||||
|
||||
package PBot::AntiFlood;
|
||||
|
||||
@ -15,8 +14,6 @@ use strict;
|
||||
|
||||
use feature 'switch';
|
||||
|
||||
use Storable;
|
||||
|
||||
use vars qw($VERSION);
|
||||
$VERSION = $PBot::PBot::VERSION;
|
||||
|
||||
@ -29,7 +26,7 @@ use Carp ();
|
||||
|
||||
sub new {
|
||||
if(ref($_[1]) eq 'HASH') {
|
||||
Carp::croak("Options to AntiFlood should be key/value pairs, not hash reference");
|
||||
Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference");
|
||||
}
|
||||
|
||||
my ($class, %conf) = @_;
|
||||
@ -42,32 +39,26 @@ sub new {
|
||||
sub initialize {
|
||||
my ($self, %conf) = @_;
|
||||
|
||||
my $pbot = delete $conf{pbot};
|
||||
if(not defined $pbot) {
|
||||
Carp::croak("Missing pbot reference to AntiFlood");
|
||||
}
|
||||
$self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__);
|
||||
|
||||
$self->{pbot} = $pbot;
|
||||
$self->{FLOOD_IGNORE} = -1;
|
||||
$self->{FLOOD_CHAT} = 0;
|
||||
$self->{FLOOD_JOIN} = 1;
|
||||
# flags for 'validated' field
|
||||
$self->{NICKSERV_VALIDATED} = (1<<0);
|
||||
$self->{NEEDS_CHECKBAN} = (1<<1);
|
||||
|
||||
$self->{ENTER_ABUSE_MAX_LINES} = 4;
|
||||
$self->{ENTER_ABUSE_MAX_LINES} = 4;
|
||||
$self->{ENTER_ABUSE_MAX_OFFENSES} = 3;
|
||||
$self->{ENTER_ABUSE_MAX_SECONDS} = 20;
|
||||
$self->{ENTER_ABUSE_MAX_SECONDS} = 20;
|
||||
|
||||
$self->load_message_history;
|
||||
$self->{channels} = {}; # per-channel statistics, e.g. for optimized tracking of last spoken nick, etc
|
||||
$self->{channels} = {}; # per-channel statistics, e.g. for optimized tracking of last spoken nick for enter-abuse detection, etc
|
||||
|
||||
my $filename = delete $conf{filename} // $self->{pbot}->{data_dir} . '/ban_whitelist';
|
||||
my $filename = delete $conf{banwhitelist_file} // $self->{pbot}->{data_dir} . '/ban_whitelist';
|
||||
$self->{ban_whitelist} = PBot::DualIndexHashObject->new(name => 'BanWhitelist', filename => $filename);
|
||||
$self->{ban_whitelist}->load;
|
||||
|
||||
$pbot->timer->register(sub { $self->prune_message_history }, 60 * 60 * 1);
|
||||
$self->{pbot}->timer->register(sub { $self->adjust_offenses }, 60 * 60 * 1);
|
||||
|
||||
$pbot->commands->register(sub { return $self->unbanme(@_) }, "unbanme", 0);
|
||||
$pbot->commands->register(sub { return $self->whitelist(@_) }, "whitelist", 10);
|
||||
$pbot->commands->register(sub { return $self->save_message_history_cmd(@_) }, "save_message_history", 60);
|
||||
$self->{pbot}->commands->register(sub { return $self->unbanme(@_) }, "unbanme", 0);
|
||||
$self->{pbot}->commands->register(sub { return $self->whitelist(@_) }, "whitelist", 10);
|
||||
}
|
||||
|
||||
sub ban_whitelisted {
|
||||
@ -135,136 +126,68 @@ sub whitelist {
|
||||
}
|
||||
}
|
||||
|
||||
sub get_flood_account {
|
||||
my ($self, $nick, $user, $host) = @_;
|
||||
|
||||
return "$nick!$user\@$host" if exists $self->message_history->{"$nick!$user\@$host"};
|
||||
|
||||
my $found_link;
|
||||
foreach my $mask (keys %{ $self->message_history }) {
|
||||
# check if foo!bar@baz matches foo!*@*; e.g., same nick, but possibly different user@host
|
||||
# (usually logging into nickserv or a dynamic ip address, but could possibly be attempted nick hijacking)
|
||||
if($mask =~ m/^\Q$nick\E!.*/i) {
|
||||
#$self->{pbot}->logger->log("anti-flood: [get-account] $nick!$user\@$host seen previously as $mask\n");
|
||||
$found_link = 1;
|
||||
}
|
||||
|
||||
# check if foo!bar@baz matches *!bar@baz; e.g., same user@host, but different nick
|
||||
# (usually alternate-nicks due to rejoining)
|
||||
if($mask =~ m/!\Q$user\E@\Q$host\E$/i) {
|
||||
$found_link = 1;
|
||||
}
|
||||
|
||||
if($found_link) {
|
||||
$self->{pbot}->logger->log("anti-flood: [get-account] $nick!$user\@$host linked to $mask\n");
|
||||
$self->{message_history}->{"$nick!$user\@$host"} = $self->{message_history}->{$mask};
|
||||
|
||||
foreach my $channel (keys %{ $self->{message_history}->{$mask}->{channels} }) {
|
||||
$self->{message_history}->{$mask}->{channels}->{$channel}{validated} = 0;
|
||||
}
|
||||
|
||||
if(exists $self->{message_history}->{$mask}->{nickserv_accounts}) {
|
||||
foreach my $nickserv_account (keys $self->{message_history}->{$mask}->{nickserv_accounts}) {
|
||||
$self->check_nickserv_accounts($nick, $nickserv_account, "$nick!$user\@$host");
|
||||
}
|
||||
}
|
||||
|
||||
return "$nick!$user\@$host";
|
||||
}
|
||||
}
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub add_message {
|
||||
sub check_join_watch {
|
||||
my ($self, $account, $channel, $text, $mode) = @_;
|
||||
my $now = gettimeofday;
|
||||
|
||||
return undef if $channel =~ /[@!]/; # ignore QUIT messages from nick!user@host channels
|
||||
return if $channel =~ /[@!]/; # ignore QUIT messages from nick!user@host channels
|
||||
|
||||
$text =~ s/^$self->{pbot}->{trigger}login\s+\S+/$self->{pbot}->{trigger}login <redacted>/; # redact login passwords (e.g., from `recall` command, etc)
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'join_watch');
|
||||
|
||||
if(not exists $self->message_history->{$account}->{channels}->{$channel}) {
|
||||
#$self->{pbot}->logger->log("adding new channel for existing nick\n");
|
||||
$self->message_history->{$account}->{channels}->{$channel}{offenses} = 0;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{last_offense_timestamp} = 0;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch} = 0;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{messages} = [];
|
||||
}
|
||||
|
||||
#$self->{pbot}->logger->log("appending new message\n");
|
||||
push(@{ $self->message_history->{$account}->{channels}->{$channel}{messages} }, { timestamp => $now, msg => $text, mode => $mode });
|
||||
$self->message_history->{$account}->{channels}->{$channel}{last_spoken} = $now;
|
||||
|
||||
my $length = $#{ $self->message_history->{$account}->{channels}->{$channel}{messages} } + 1;
|
||||
|
||||
if($mode == $self->{FLOOD_JOIN}) {
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN}) {
|
||||
if($text =~ /^JOIN/) {
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch}++;
|
||||
$channel_data->{join_watch}++;
|
||||
$self->{pbot}->logger->log("Join watch incremented to $channel_data->{join_watch} for $account\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
} else {
|
||||
# PART or QUIT
|
||||
# check QUIT message for netsplits, and decrement joinwatch if found
|
||||
# check QUIT message for netsplits, and decrement joinwatch to allow a free rejoin
|
||||
if($text =~ /^QUIT .*\.net .*\.split/) {
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch}--;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch} = 0 if $self->message_history->{$account}->{channels}->{$channel}{join_watch} < 0;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{messages}->[$length - 1]{mode} = $self->{FLOOD_IGNORE};
|
||||
if($channel_data->{join_watch} > 0) {
|
||||
$channel_data->{join_watch}--;
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
# check QUIT message for Ping timeout or Excess Flood
|
||||
elsif($text =~ /^QUIT Ping timeout/ or $text =~ /^QUIT Excess Flood/) {
|
||||
# ignore these (used to treat aggressively)
|
||||
$self->message_history->{$account}->{channels}->{$channel}{messages}->[$length - 1]{mode} = $self->{FLOOD_IGNORE};
|
||||
} else {
|
||||
# some other type of QUIT or PART
|
||||
$self->message_history->{$account}->{channels}->{$channel}{messages}->[$length - 1]{mode} = $self->{FLOOD_IGNORE};
|
||||
}
|
||||
}
|
||||
} elsif($mode == $self->{FLOOD_CHAT}) {
|
||||
} elsif($mode == $self->{pbot}->{messagehistory}->{MSG_CHAT}) {
|
||||
# reset joinwatch if they send a message
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch} = 0;
|
||||
if($channel_data->{join_watch} > 0) {
|
||||
$channel_data->{join_watch} = 0;
|
||||
$self->{pbot}->logger->log("Join watch reset for $account\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
|
||||
# keep only MAX_NICK_MESSAGES message history per channel
|
||||
while($length >= $self->{pbot}->{MAX_NICK_MESSAGES}) {
|
||||
my %msg = %{ shift(@{ $self->message_history->{$account}->{channels}->{$channel}{messages} }) };
|
||||
#$self->{pbot}->logger->log("shifting message off top: $msg{msg}, $msg{timestamp}\n");
|
||||
$length--;
|
||||
}
|
||||
|
||||
return $length;
|
||||
}
|
||||
|
||||
sub check_flood {
|
||||
my ($self, $channel, $nick, $user, $host, $text, $max_messages, $max_time, $mode) = @_;
|
||||
|
||||
$channel = lc $channel;
|
||||
my $mask = "$nick!$user\@$host";
|
||||
|
||||
my $mask = "$nick!$user\@$host";
|
||||
$self->{pbot}->logger->log(sprintf("%-14s | %-65s | %s\n", $channel eq $mask ? "QUIT" : $channel, $mask, $text));
|
||||
|
||||
my $account = $self->get_flood_account($nick, $user, $host);
|
||||
|
||||
if(not defined $account) {
|
||||
$account = $mask;
|
||||
}
|
||||
my $account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host);
|
||||
|
||||
# handle QUIT events
|
||||
# (these events come from $channel nick!user@host, not a specific channel or nick,
|
||||
# so they need to be dispatched to all channels the nick has been seen on)
|
||||
if($mode == $self->{FLOOD_JOIN} and $text =~ /^QUIT/) {
|
||||
return if not exists $self->message_history->{$account}; # don't create empty account
|
||||
foreach my $chan (keys %{ $self->message_history->{$account}->{channels} }) {
|
||||
next if $chan !~ m/^#/; # skip non-channels (private messages, etc)
|
||||
$self->add_message($account, $chan, $text, $mode);
|
||||
# remove validation on QUITs so we check for ban-evasion when user returns at a later time
|
||||
$self->message_history->{$account}->{channels}->{$chan}{validated} = 0;
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN} and $text =~ /^QUIT/) {
|
||||
my @channels = $self->{pbot}->{messagehistory}->{database}->get_channels($account);
|
||||
foreach my $chan (@channels) {
|
||||
$self->check_join_watch($account, $chan, $text, $mode);
|
||||
}
|
||||
|
||||
$self->{pbot}->{messagehistory}->{database}->devalidate_all_channels($account);
|
||||
# don't do flood processing for QUIT events
|
||||
return;
|
||||
}
|
||||
|
||||
my $length = $self->add_message($account, $channel, $text, $mode);
|
||||
return if not defined $length;
|
||||
$self->check_join_watch($account, $channel, $text, $mode);
|
||||
|
||||
# do not do flood processing for bot messages
|
||||
if($nick eq $self->{pbot}->botnick) {
|
||||
@ -275,9 +198,28 @@ sub check_flood {
|
||||
# do not do flood processing if channel is not in bot's channel list or bot is not set as chanop for the channel
|
||||
return if ($channel =~ /^#/) and (not exists $self->{pbot}->channels->channels->hash->{$channel} or $self->{pbot}->channels->channels->hash->{$channel}{chanop} == 0);
|
||||
|
||||
if($channel =~ /^#/ and $mode == $self->{FLOOD_JOIN} and $text =~ /^PART/) {
|
||||
if($channel =~ /^#/ and $mode == $self->{pbot}->{messagehistory}->{MSG_JOIN} and $text =~ /^PART/) {
|
||||
# remove validation on PART so we check for ban-evasion when user returns at a later time
|
||||
$self->message_history->{$account}->{channels}->{$channel}{validated} = 0;
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'validated');
|
||||
if($channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
|
||||
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
|
||||
if($max_messages > $self->{pbot}->{MAX_NICK_MESSAGES}) {
|
||||
$self->{pbot}->logger->log("Warning: max_messages greater than MAX_NICK_MESSAGES; truncating.\n");
|
||||
$max_messages = $self->{pbot}->{MAX_NICK_MESSAGES};
|
||||
}
|
||||
|
||||
# check for ban evasion if channel begins with # (not private message) and hasn't yet been validated against ban evasion
|
||||
if($channel =~ m/^#/ and not $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'validated')->{'validated'} & $self->{NICKSERV_VALIDATED}) {
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN} and $text =~ /^PART/) {
|
||||
# don't check for evasion on PARTs
|
||||
} else {
|
||||
$self->{pbot}->conn->whois($nick);
|
||||
$self->check_bans($account, $mask, $channel);
|
||||
}
|
||||
}
|
||||
|
||||
# do not do flood enforcement for this event if bot is lagging
|
||||
@ -286,113 +228,114 @@ sub check_flood {
|
||||
return;
|
||||
}
|
||||
|
||||
if($max_messages > $self->{pbot}->{MAX_NICK_MESSAGES}) {
|
||||
$self->{pbot}->logger->log("Warning: max_messages greater than MAX_NICK_MESSAGES; truncating.\n");
|
||||
$max_messages = $self->{pbot}->{MAX_NICK_MESSAGES};
|
||||
}
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_CHAT} and $channel =~ m/^#/) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'enter_abuse', 'enter_abuses');
|
||||
|
||||
# check for ban evasion if channel begins with # (not private message) and hasn't yet been validated against ban evasion
|
||||
if($channel =~ m/^#/ and not $self->message_history->{$account}->{channels}->{$channel}{validated}) {
|
||||
if($mode == $self->{FLOOD_JOIN} and $text =~ /^PART/) {
|
||||
# don't check for evasion on PARTs
|
||||
} else {
|
||||
$self->{pbot}->conn->whois($nick);
|
||||
$self->check_bans($account, $channel);
|
||||
}
|
||||
}
|
||||
|
||||
if($mode == $self->{FLOOD_CHAT} and $channel =~ m/^#/) {
|
||||
if(defined $self->{channels}->{$channel}->{last_spoken_nick} and $nick eq $self->{channels}->{$channel}->{last_spoken_nick}) {
|
||||
my %msg = %{ @{ $self->message_history->{$account}->{channels}->{$channel}{messages} }[$length - 2] };
|
||||
my %last = %{ @{ $self->message_history->{$account}->{channels}->{$channel}{messages} }[$length - 1] };
|
||||
my $messages = $self->{pbot}->{messagehistory}->{database}->get_recent_messages($account, $channel, 2, $self->{pbot}->{messagehistory}->{MSG_CHAT});
|
||||
|
||||
if($last{timestamp} - $msg{timestamp} <= $self->{ENTER_ABUSE_MAX_SECONDS}) {
|
||||
if(++$self->message_history->{$account}->{channels}->{$channel}{enter_abuse} >= $self->{ENTER_ABUSE_MAX_LINES} - 1) {
|
||||
$self->message_history->{$account}->{channels}->{$channel}{enter_abuse} = $self->{ENTER_ABUSE_MAX_LINES} / 2 - 1;
|
||||
if(++$self->message_history->{$account}->{channels}->{$channel}{enter_abuses} >= $self->{ENTER_ABUSE_MAX_OFFENSES}) {
|
||||
my $offenses = $self->message_history->{$account}->{channels}->{$channel}{enter_abuses} - $self->{ENTER_ABUSE_MAX_OFFENSES} + 1;
|
||||
if($messages->[1]->{timestamp} - $messages->[0]->{timestamp} <= $self->{ENTER_ABUSE_MAX_SECONDS}) {
|
||||
if(++$channel_data->{enter_abuse} >= $self->{ENTER_ABUSE_MAX_LINES} - 1) {
|
||||
$channel_data->{enter_abuse} = $self->{ENTER_ABUSE_MAX_LINES} / 2 - 1;
|
||||
if(++$channel_data->{enter_abuses} >= $self->{ENTER_ABUSE_MAX_OFFENSES}) {
|
||||
my $offenses = $channel_data->{enter_abuses} - $self->{ENTER_ABUSE_MAX_OFFENSES} + 1;
|
||||
my $ban_length = $offenses ** $offenses * $offenses * 30;
|
||||
$self->{pbot}->chanops->ban_user_timed("*!$user\@$host", $channel, $ban_length);
|
||||
$ban_length = duration($ban_length);
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse offense " . $self->message_history->{$account}->{channels}->{$channel}{enter_abuses} . " earned $ban_length ban\n");
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse offense " . $channel_data->{enter_abuses} . " earned $ban_length ban\n");
|
||||
$self->{pbot}->conn->privmsg($nick, "You have been muted due to abusing the enter key. Please do not split your sentences over multiple messages. You will be allowed to speak again in $ban_length.");
|
||||
} else {
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuses counter incremented to " . $self->message_history->{$account}->{channels}->{$channel}{enter_abuses} . "\n");
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuses counter incremented to " . $channel_data->{enter_abuses} . "\n");
|
||||
}
|
||||
} else {
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse counter incremented to " . $self->message_history->{$account}->{channels}->{$channel}{enter_abuse} . "\n");
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse counter incremented to " . $channel_data->{enter_abuse} . "\n");
|
||||
}
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
} else {
|
||||
if($channel_data->{enter_abuse} > 0) {
|
||||
$self->{pbot}->logger->log("$nick $channel more than $self->{ENTER_ABUSE_MAX_SECONDS} seconds since last message, enter abuse counter reset\n");
|
||||
$self->message_history->{$account}->{channels}->{$channel}{enter_abuse} = 0;
|
||||
$channel_data->{enter_abuse} = 0;
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$self->{channels}->{$channel}->{last_spoken_nick} = $nick;
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse counter reset\n") if defined $self->message_history->{$account}->{channels}->{$channel}{enter_abuse} and $self->message_history->{$account}->{channels}->{$channel}{enter_abuse} > 0;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{enter_abuse} = 0;
|
||||
if($channel_data->{enter_abuse} > 0) {
|
||||
$self->{pbot}->logger->log("$nick $channel enter abuse counter reset\n");
|
||||
$channel_data->{enter_abuse} = 0;
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($max_messages > 0 and $length >= $max_messages) {
|
||||
# $self->{pbot}->logger->log("More than $max_messages messages, comparing time differences ($max_time)\n") if $mode == $self->{FLOOD_JOIN};
|
||||
|
||||
my %msg;
|
||||
if($mode == $self->{FLOOD_CHAT}) {
|
||||
%msg = %{ @{ $self->message_history->{$account}->{channels}->{$channel}{messages} }[$length - $max_messages] };
|
||||
if($max_messages > 0 and $self->{pbot}->{messagehistory}->{database}->get_max_messages($account, $channel) >= $max_messages) {
|
||||
my $msg;
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_CHAT}) {
|
||||
$msg = $self->{pbot}->{messagehistory}->{database}->recall_message_by_count($account, $channel, $max_messages - 1)
|
||||
}
|
||||
elsif($mode == $self->{FLOOD_JOIN}) {
|
||||
my $count = 0;
|
||||
my $i = $length - 1;
|
||||
# $self->{pbot}->logger->log("Checking flood history, i = $i\n") if $self->message_history->{$account}->{channels}->{$channel}{join_watch} >= $max_messages;
|
||||
for(; $i >= 0; $i--) {
|
||||
# $self->{pbot}->logger->log($i . " " . $self->message_history->{$account}->{channels}->{$channel}{messages}->[$i]{mode} ." " . $self->message_history->{$account}->{channels}->{$channel}{messages}->[$i]{msg} . " " . $self->message_history->{$account}->{channels}->{$channel}{messages}->[$i]{timestamp} . " [" . ago_exact(time - $self->message_history->{$account}->{channels}->{$channel}{messages}->[$i]{timestamp}) . "]\n") if $self->message_history->{$account}->{channels}->{$channel}{join_watch} >= $max_messages;
|
||||
next if $self->message_history->{$account}->{channels}->{$channel}{messages}->[$i]{mode} != $self->{FLOOD_JOIN};
|
||||
last if ++$count >= 4;
|
||||
}
|
||||
$i = 0 if $i < 0;
|
||||
%msg = %{ @{ $self->message_history->{$account}->{channels}->{$channel}{messages} }[$i] };
|
||||
elsif($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN}) {
|
||||
my $joins = $self->{pbot}->{messagehistory}->{database}->get_recent_messages($account, $channel, $max_messages, $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
$msg = $joins->[0];
|
||||
}
|
||||
else {
|
||||
$self->{pbot}->logger->log("Unknown flood mode [$mode] ... aborting flood enforcement.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
my %last = %{ @{ $self->message_history->{$account}->{channels}->{$channel}{messages} }[$length - 1] };
|
||||
my $last = $self->{pbot}->{messagehistory}->{database}->recall_message_by_count($account, $channel, 0);
|
||||
|
||||
if($last{timestamp} - $msg{timestamp} <= $max_time && not $self->{pbot}->admins->loggedin($channel, "$nick!$user\@$host")) {
|
||||
if($mode == $self->{FLOOD_JOIN}) {
|
||||
if($self->message_history->{$account}->{channels}->{$channel}{join_watch} >= $max_messages) {
|
||||
$self->message_history->{$account}->{channels}->{$channel}{offenses}++;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{last_offense_timestamp} = gettimeofday;
|
||||
$self->{pbot}->logger->log(" msg: [$msg->{timestamp}] $msg->{msg}\n");
|
||||
$self->{pbot}->logger->log("last: [$last->{timestamp}] $last->{msg}\n");
|
||||
$self->{pbot}->logger->log("Comparing message timestamps $last->{timestamp} - $msg->{timestamp} = " . ($last->{timestamp} - $msg->{timestamp}) . " against max_time $max_time\n");
|
||||
|
||||
my $timeout = (2 ** (($self->message_history->{$account}->{channels}->{$channel}{offenses} + 2) < 10 ? $self->message_history->{$account}->{channels}->{$channel}{offenses} + 2 : 10));
|
||||
if($last->{timestamp} - $msg->{timestamp} <= $max_time && not $self->{pbot}->admins->loggedin($channel, "$nick!$user\@$host")) {
|
||||
if($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN}) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'offenses', 'last_offense', 'join_watch');
|
||||
$self->{pbot}->{logger}->log("$account offenses $channel_data->{offenses}, join watch $channel_data->{join_watch}, max messages $max_messages\n");
|
||||
if($channel_data->{join_watch} >= $max_messages) {
|
||||
$channel_data->{offenses}++;
|
||||
$channel_data->{last_offense} = gettimeofday;
|
||||
|
||||
my $timeout = (2 ** (($channel_data->{offenses} + 2) < 10 ? $channel_data->{offenses} + 2 : 10));
|
||||
my $banmask = address_to_mask($host);
|
||||
|
||||
$self->{pbot}->chanops->ban_user_timed("*!$user\@$banmask\$##stop_join_flood", $channel, $timeout * 60 * 60);
|
||||
$self->{pbot}->logger->log("$nick!$user\@$banmask banned for $timeout hours due to join flooding (offense #" . $self->message_history->{$account}->{channels}->{$channel}{offenses} . ").\n");
|
||||
$self->{pbot}->logger->log("$nick!$user\@$banmask banned for $timeout hours due to join flooding (offense #" . $channel_data->{offenses} . ").\n");
|
||||
$self->{pbot}->conn->privmsg($nick, "You have been banned from $channel due to join flooding. If your connection issues have been fixed, or this was an accident, you may request an unban at any time by responding to this message with: unbanme $channel, otherwise you will be automatically unbanned in $timeout hours.");
|
||||
$self->message_history->{$account}->{channels}->{$channel}{join_watch} = $max_messages - 2; # give them a chance to rejoin
|
||||
$channel_data->{join_watch} = $max_messages - 2; # give them a chance to rejoin
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
} elsif($mode == $self->{FLOOD_CHAT}) {
|
||||
# don't increment offenses again if already banned
|
||||
return if $self->{pbot}->chanops->{unban_timeout}->find_index($channel, "*!$user\@$host");
|
||||
|
||||
$self->message_history->{$account}->{channels}->{$channel}{offenses}++;
|
||||
$self->message_history->{$account}->{channels}->{$channel}{last_offense_timestamp} = gettimeofday;
|
||||
|
||||
my $length = $self->message_history->{$account}->{channels}->{$channel}{offenses} ** $self->message_history->{$account}->{channels}->{$channel}{offenses} * $self->message_history->{$account}->{channels}->{$channel}{offenses} * 30;
|
||||
|
||||
} elsif($mode == $self->{pbot}->{messagehistory}->{MSG_CHAT}) {
|
||||
if($channel =~ /^#/) { #channel flood (opposed to private message or otherwise)
|
||||
# don't increment offenses again if already banned
|
||||
return if $self->{pbot}->chanops->{unban_timeout}->find_index($channel, "*!$user\@$host");
|
||||
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'offenses', 'last_offense');
|
||||
$channel_data->{offenses}++;
|
||||
$channel_data->{last_offense} = gettimeofday;
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
|
||||
my $length = $channel_data->{offenses} ** $channel_data->{offenses} * $channel_data->{offenses} * 30;
|
||||
|
||||
$self->{pbot}->chanops->ban_user_timed("*!$user\@$host", $channel, $length);
|
||||
$length = duration($length);
|
||||
$self->{pbot}->logger->log("$nick $channel flood offense " . $self->message_history->{$account}->{channels}->{$channel}{offenses} . " earned $length ban\n");
|
||||
$self->{pbot}->logger->log("$nick $channel flood offense " . $channel_data->{offenses} . " earned $length ban\n");
|
||||
$self->{pbot}->conn->privmsg($nick, "You have been muted due to flooding. Please use a web paste service such as http://codepad.org for lengthy pastes. You will be allowed to speak again in $length.");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
else { # private message flood
|
||||
return if exists ${ $self->{pbot}->ignorelist->{ignore_list} }{"$nick!$user\@$host"}{$channel};
|
||||
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'offenses', 'last_offense');
|
||||
$channel_data->{offenses}++;
|
||||
$channel_data->{last_offense} = gettimeofday;
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
|
||||
my $length = $channel_data->{offenses} ** $channel_data->{offenses} * $channel_data->{offenses} * 30;
|
||||
|
||||
$self->{pbot}->{ignorelistcmds}->ignore_user("", "floodcontrol", "", "", "$nick!$user\@$host $channel $length");
|
||||
$length = duration($length);
|
||||
$self->{pbot}->logger->log("$nick msg flood offense " . $self->message_history->{$account}->{channels}->{$channel}{offenses} . " earned $length ignore\n");
|
||||
$self->{pbot}->logger->log("$nick msg flood offense " . $channel_data->{offenses} . " earned $length ignore\n");
|
||||
$self->{pbot}->conn->privmsg($nick, "You have used too many commands in too short a time period, you have been ignored for $length.");
|
||||
}
|
||||
}
|
||||
@ -400,60 +343,6 @@ sub check_flood {
|
||||
}
|
||||
}
|
||||
|
||||
sub message_history {
|
||||
my $self = shift;
|
||||
return $self->{message_history};
|
||||
}
|
||||
|
||||
sub prune_message_history {
|
||||
my $self = shift;
|
||||
|
||||
$self->{pbot}->logger->log("Pruning message history . . .\n");
|
||||
|
||||
foreach my $mask (keys %{ $self->{message_history} }) {
|
||||
foreach my $channel (keys %{ $self->{message_history}->{$mask}->{channels} }) {
|
||||
my $length = $#{ $self->{message_history}->{$mask}->{channels}->{$channel}{messages} } + 1;
|
||||
|
||||
if($length <= 0) {
|
||||
$self->{pbot}->logger->log("[prune-message-history] $mask in $channel has no messages, removing channel entry\n");
|
||||
delete $self->{message_history}->{$mask}->{channels}->{$channel};
|
||||
next;
|
||||
}
|
||||
|
||||
my %last = %{ @{ $self->{message_history}->{$mask}->{channels}->{$channel}{messages} }[$length - 1] };
|
||||
|
||||
# delete channel key if no activity for a while
|
||||
if(gettimeofday - $last{timestamp} >= 60 * 60 * 24 * 90) {
|
||||
$self->{pbot}->logger->log("[prune-message-history] $mask in $channel hasn't spoken in ninety days; removing channel history.\n");
|
||||
delete $self->{message_history}->{$mask}->{channels}->{$channel};
|
||||
next;
|
||||
}
|
||||
|
||||
# decrease offenses counter if 24 hours of elapsed without any new offense
|
||||
if ($self->{message_history}->{$mask}->{channels}->{$channel}{offenses} > 0 and
|
||||
$self->{message_history}->{$mask}->{channels}->{$channel}{last_offense_timestamp} > 0 and
|
||||
(gettimeofday - $self->{message_history}->{$mask}->{channels}->{$channel}{last_offense_timestamp} >= 60 * 60 * 24)) {
|
||||
$self->{message_history}->{$mask}->{channels}->{$channel}{offenses}--;
|
||||
$self->{message_history}->{$mask}->{channels}->{$channel}{last_offense_timestamp} = gettimeofday;
|
||||
$self->{pbot}->logger->log("[prune-message-history] [$channel][$mask] 24 hours since last offense/decrease -- decreasing offenses to $self->{message_history}->{$mask}->{channels}->{$channel}{offenses}\n");
|
||||
}
|
||||
|
||||
# decrease enter abuses counter once an hour
|
||||
if(defined $self->message_history->{$mask}->{channels}->{$channel}{enter_abuses} and $self->message_history->{$mask}->{channels}->{$channel}{enter_abuses} > 0) {
|
||||
$self->message_history->{$mask}->{channels}->{$channel}{enter_abuses}--;
|
||||
$self->{pbot}->logger->log("[prune-message-history] [$channel][$mask] decreasing enter abuse offenses to $self->{message_history}->{$mask}->{channels}->{$channel}{enter_abuses}\n");
|
||||
}
|
||||
}
|
||||
|
||||
# delete account for this $mask if all its channels have been deleted
|
||||
if(scalar keys %{ $self->{message_history}->{$mask} } == 0) {
|
||||
$self->{pbot}->logger->log("[prune-message-history] $mask has no more channels remaining; deleting history account.\n");
|
||||
delete $self->{message_history}->{$mask};
|
||||
}
|
||||
}
|
||||
$self->save_message_history;
|
||||
}
|
||||
|
||||
sub unbanme {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
my $channel = lc $arguments;
|
||||
@ -470,14 +359,10 @@ sub unbanme {
|
||||
return "/msg $nick There is no temporary ban set for $mask in channel $channel.";
|
||||
}
|
||||
|
||||
my $nickserv_accounts;
|
||||
if(exists $self->{message_history}->{"$nick!$user\@$host"}->{nickserv_accounts}) {
|
||||
$nickserv_accounts = $self->{message_history}->{"$nick!$user\@$host"}->{nickserv_accounts};
|
||||
} else {
|
||||
$nickserv_accounts->{-1} = undef;
|
||||
}
|
||||
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);
|
||||
|
||||
foreach my $nickserv_account (keys $nickserv_accounts) {
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
my $baninfos = $self->{pbot}->bantracker->get_baninfo("$nick!$user\@$host", $channel, $nickserv_account);
|
||||
|
||||
if(defined $baninfos) {
|
||||
@ -495,8 +380,8 @@ sub unbanme {
|
||||
}
|
||||
}
|
||||
|
||||
my $account = $self->get_flood_account($nick, $user, $host);
|
||||
if(defined $account and $self->message_history->{$account}->{channels}->{$channel}{offenses} > 2) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'offenses');
|
||||
if($channel_data->{offenses} > 2) {
|
||||
return "/msg $nick You may only use unbanme for the first two offenses. You will be automatically unbanned in a few hours, and your offense counter will decrement once every 24 hours.";
|
||||
}
|
||||
|
||||
@ -528,166 +413,149 @@ sub address_to_mask {
|
||||
sub devalidate_accounts {
|
||||
# remove validation on accounts in $channel that match a ban/quiet $mask
|
||||
my ($self, $mask, $channel) = @_;
|
||||
my ($nickserv_accounts, $ban_account);
|
||||
my @message_accounts;
|
||||
|
||||
$self->{pbot}->logger->log("Devalidating accounts for $mask in $channel\n");
|
||||
|
||||
if($mask =~ m/^\$a:(.*)/) {
|
||||
$ban_account = lc $1;
|
||||
my $ban_account = lc $1;
|
||||
@message_accounts = $self->{pbot}->{messagehistory}->{database}->find_message_accounts_by_nickserv($ban_account);
|
||||
} else {
|
||||
$ban_account = undef;
|
||||
@message_accounts = $self->{pbot}->{messagehistory}->{database}->find_message_accounts_by_mask($mask);
|
||||
}
|
||||
|
||||
my $mask_original = $mask;
|
||||
$mask = quotemeta $mask;
|
||||
$mask =~ s/\\\*/.*?/g;
|
||||
$mask =~ s/\\\?/./g;
|
||||
|
||||
foreach my $account (keys %{ $self->{message_history} }) {
|
||||
if(exists $self->{message_history}->{$account}->{channels}->{$channel}) {
|
||||
if(defined $ban_account and exists $self->{message_history}->{$account}->{nickserv_accounts}) {
|
||||
$nickserv_accounts = $self->{message_history}->{$account}->{nickserv_accounts};
|
||||
} else {
|
||||
$nickserv_accounts = undef;
|
||||
}
|
||||
|
||||
my $devalidate = 0;
|
||||
if(defined $ban_account and defined $nickserv_accounts) {
|
||||
foreach my $nickserv_account (keys $nickserv_accounts) {
|
||||
if($nickserv_account eq $ban_account) {
|
||||
$devalidate = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($devalidate or $account =~ m/^$mask$/i) {
|
||||
$self->{pbot}->logger->log("anti-flood: [devalidate-accounts] $account matches $mask_original in $channel, devalidating\n");
|
||||
$self->message_history->{$account}->{channels}->{$channel}{validated} = 0;
|
||||
}
|
||||
foreach my $account (@message_accounts) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($account, $channel, 'validated');
|
||||
if(defined $channel_data and $channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
|
||||
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
|
||||
$self->{pbot}->logger->log("Devalidating account $account\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub check_bans {
|
||||
my ($self, $mask, $channel) = @_;
|
||||
my ($bans, @nickserv_accounts, $nick, $host, $do_not_validate);
|
||||
my ($self, $message_account, $mask, $channel) = @_;
|
||||
|
||||
# $self->{pbot}->logger->log("anti-flood: [check-bans] checking for bans on $mask in $channel\n");
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] checking for bans on $mask in $channel\n");
|
||||
|
||||
if(exists $self->{message_history}->{$mask}->{nickserv_accounts}) {
|
||||
foreach my $account (keys $self->{message_history}->{$mask}->{nickserv_accounts}) {
|
||||
# $self->{pbot}->logger->log("anti-flood: [check-bans] $mask is using account $account\n");
|
||||
push @nickserv_accounts, $account;
|
||||
my @nickserv_accounts = $self->{pbot}->{messagehistory}->{database}->get_nickserv_accounts($message_account);
|
||||
my $current_nickserv_account = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($message_account);
|
||||
|
||||
if($current_nickserv_account) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] current nickserv [$current_nickserv_account] found for $mask\n");
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if($channel_data->{validated} & $self->{NEEDS_CHECKBAN}) {
|
||||
$channel_data->{validated} &= ~$self->{NEEDS_CHECKBAN};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
|
||||
}
|
||||
delete $self->{message_history}->{$mask}->{channels}->{$channel}{needs_validation};
|
||||
} else {
|
||||
# mark this account as needing check-bans when nickserv account is identified
|
||||
$self->{message_history}->{$mask}->{channels}->{$channel}{needs_validation} = 1;
|
||||
# $self->{pbot}->logger->log("anti-flood: [check-bans] no account for $mask; marking for later validation\n");
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if(not $channel_data->{validated} & $self->{NEEDS_CHECKBAN}) {
|
||||
$channel_data->{validated} |= $self->{NEEDS_CHECKBAN};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
|
||||
}
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] no account for $mask; marking for later validation\n");
|
||||
}
|
||||
|
||||
($nick, $host) = $mask =~ m/^([^!]+)![^@]+\@(.*)$/;
|
||||
my ($nick, $host) = $mask =~ m/^([^!]+)![^@]+\@(.*)$/;
|
||||
|
||||
foreach my $account (keys %{ $self->{message_history} }) {
|
||||
if(exists $self->{message_history}->{$account}->{channels}->{$channel}) {
|
||||
my $check_ban = 0;
|
||||
my $target_nickserv_accounts;
|
||||
my $hostmasks = $self->{pbot}->{messagehistory}->{database}->get_hostmasks_for_channel($channel);
|
||||
|
||||
# check if nickserv accounts match
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
if(exists $self->{message_history}->{$account}->{nickserv_accounts}) {
|
||||
foreach my $key ($self->{message_history}->{$account}->{nickserv_accounts}) {
|
||||
if($key eq $nickserv_account) {
|
||||
#$self->{pbot}->logger->log("anti-flood: [check-bans] nickserv account for $account matches $nickserv_account\n");
|
||||
$target_nickserv_accounts = $self->{message_history}->{$account}->{nickserv_accounts};
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
}
|
||||
}
|
||||
my ($do_not_validate, $bans);
|
||||
foreach my $hostmask (@$hostmasks) {
|
||||
my @hostmask_nickserv_accounts = $self->{pbot}->{messagehistory}->{database}->get_nickserv_accounts($hostmask->{id});
|
||||
my $check_ban = 0;
|
||||
|
||||
# check if nickserv accounts match
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
foreach my $key (@hostmask_nickserv_accounts) {
|
||||
if($key eq $nickserv_account) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] nickserv account for $hostmask->{hostmask} matches $nickserv_account\n");
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# check if hosts match
|
||||
my ($account_host) = $account =~ m/\@(.*)$/;
|
||||
|
||||
if($host eq $account_host) {
|
||||
#$self->{pbot}->logger->log("anti-flood: [check-bans] host for $account matches $mask\n");
|
||||
if(exists $self->{message_history}->{$account}->{nickserv_accounts}) {
|
||||
$target_nickserv_accounts = $self->{message_history}->{$account}->{nickserv_accounts};
|
||||
}
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
# check if hosts match
|
||||
my ($account_host) = $hostmask->{hostmask} =~ m/\@(.*)$/;
|
||||
if($host eq $account_host) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] host for $hostmask->{hostmask} matches $mask\n");
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
}
|
||||
|
||||
# check if nicks match
|
||||
my ($account_nick) = $hostmask->{hostmask} =~ m/^([^!]+)/;
|
||||
if($nick eq $account_nick) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] nick for $hostmask->{hostmask} matches $mask\n");
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
}
|
||||
|
||||
CHECKBAN:
|
||||
if($check_ban) {
|
||||
if(not @hostmask_nickserv_accounts) {
|
||||
push @hostmask_nickserv_accounts, -1;
|
||||
}
|
||||
|
||||
# check if nicks match
|
||||
my ($account_nick) = $account =~ m/^([^!]+)/;
|
||||
foreach my $target_nickserv_account (@hostmask_nickserv_accounts) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] checking for bans in $channel on $hostmask->{hostmask} using $target_nickserv_account\n");
|
||||
my $baninfos = $self->{pbot}->bantracker->get_baninfo($hostmask->{hostmask}, $channel, $target_nickserv_account);
|
||||
|
||||
if($nick eq $account_nick) {
|
||||
#$self->{pbot}->logger->log("anti-flood: [check-bans] nick for $account matches $mask\n");
|
||||
if(exists $self->{message_history}->{$account}->{nickserv_accounts}) {
|
||||
$target_nickserv_accounts = $self->{message_history}->{$account}->{nickserv_accounts};
|
||||
}
|
||||
$check_ban = 1;
|
||||
goto CHECKBAN;
|
||||
}
|
||||
|
||||
CHECKBAN:
|
||||
if($check_ban) {
|
||||
if(not defined $target_nickserv_accounts) {
|
||||
$target_nickserv_accounts->{-1} = undef;
|
||||
}
|
||||
|
||||
foreach my $target_nickserv_account (keys $target_nickserv_accounts) {
|
||||
# $self->{pbot}->logger->log("anti-flood: [check-bans] checking for bans in $channel on $account using $target_nickserv_account\n");
|
||||
my $baninfos = $self->{pbot}->bantracker->get_baninfo($account, $channel, $target_nickserv_account);
|
||||
|
||||
if(defined $baninfos) {
|
||||
foreach my $baninfo (@$baninfos) {
|
||||
if(time - $baninfo->{when} < 5) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] $mask evaded $baninfo->{banmask} in $baninfo->{channel}, but within 5 seconds of establishing ban; giving another chance\n");
|
||||
$self->message_history->{$mask}->{channels}->{$channel}{validated} = 0;
|
||||
$do_not_validate = 1;
|
||||
next;
|
||||
if(defined $baninfos) {
|
||||
foreach my $baninfo (@$baninfos) {
|
||||
if(time - $baninfo->{when} < 5) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] $mask evaded $baninfo->{banmask} in $baninfo->{channel}, but within 5 seconds of establishing ban; giving another chance\n");
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if($channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
|
||||
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
|
||||
}
|
||||
|
||||
if($self->ban_whitelisted($baninfo->{channel}, $baninfo->{banmask})) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] $mask evaded $baninfo->{banmask} in $baninfo->{channel}, but allowed through whitelist\n");
|
||||
next;
|
||||
}
|
||||
|
||||
if($baninfo->{type} eq '+b' and $baninfo->{banmask} =~ m/!\*@\*$/) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Disregarding generic nick ban\n");
|
||||
next;
|
||||
}
|
||||
|
||||
my $banmask_regex = quotemeta $baninfo->{banmask};
|
||||
$banmask_regex =~ s/\\\*/.*/g;
|
||||
$banmask_regex =~ s/\\\?/./g;
|
||||
|
||||
if($baninfo->{type} eq '+q' and $mask =~ /^$banmask_regex$/i) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches quiet banmask ($banmask_regex), disregarding\n");
|
||||
next;
|
||||
}
|
||||
|
||||
my $skip_quiet_nickserv_mask = 0;
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
if($baninfo->{type} eq '+q' and $baninfo->{banmask} =~ /^\$a:(.*)/ and lc $1 eq $nickserv_account and $nickserv_account eq $self->{message_history}->{$mask}->{nickserv_account}) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches account ($nickserv_account), disregarding\n");
|
||||
$skip_quiet_nickserv_mask = 1;
|
||||
} elsif($baninfo->{type} eq '+b' and $baninfo->{banmask} =~ /^\$a:(.*)/ and lc $1 eq $nickserv_account) {
|
||||
$skip_quiet_nickserv_mask = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
next if $skip_quiet_nickserv_mask;
|
||||
|
||||
if(not defined $bans) {
|
||||
$bans = [];
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches $baninfo->{type} $baninfo->{banmask}, adding ban\n");
|
||||
push @$bans, $baninfo;
|
||||
$do_not_validate = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
if($self->ban_whitelisted($baninfo->{channel}, $baninfo->{banmask})) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] $mask evaded $baninfo->{banmask} in $baninfo->{channel}, but allowed through whitelist\n");
|
||||
next;
|
||||
}
|
||||
|
||||
if($baninfo->{type} eq '+b' and $baninfo->{banmask} =~ m/!\*@\*$/) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Disregarding generic nick ban\n");
|
||||
next;
|
||||
}
|
||||
|
||||
my $banmask_regex = quotemeta $baninfo->{banmask};
|
||||
$banmask_regex =~ s/\\\*/.*/g;
|
||||
$banmask_regex =~ s/\\\?/./g;
|
||||
|
||||
if($baninfo->{type} eq '+q' and $mask =~ /^$banmask_regex$/i) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches quiet banmask ($banmask_regex), disregarding\n");
|
||||
next;
|
||||
}
|
||||
|
||||
my $skip_quiet_nickserv_mask = 0;
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
if($baninfo->{type} eq '+q' and $baninfo->{banmask} =~ /^\$a:(.*)/ and lc $1 eq $nickserv_account and $nickserv_account eq $current_nickserv_account) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches quiet on account ($nickserv_account), disregarding\n");
|
||||
$skip_quiet_nickserv_mask = 1;
|
||||
} elsif($baninfo->{type} eq '+b' and $baninfo->{banmask} =~ /^\$a:(.*)/ and lc $1 eq $nickserv_account) {
|
||||
$skip_quiet_nickserv_mask = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
next if $skip_quiet_nickserv_mask;
|
||||
|
||||
if(not defined $bans) {
|
||||
$bans = [];
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("anti-flood: [check-bans] Hostmask ($mask) matches $baninfo->{type} $baninfo->{banmask}, adding ban\n");
|
||||
push @$bans, $baninfo;
|
||||
next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -703,55 +571,66 @@ sub check_bans {
|
||||
my ($bannick) = $mask =~ m/^([^!]+)/;
|
||||
$self->{pbot}->chanops->add_op_command($baninfo->{channel}, "kick $baninfo->{channel} $bannick Ban evasion");
|
||||
$self->{pbot}->chanops->ban_user_timed($banmask, $baninfo->{channel}, 60 * 60 * 12);
|
||||
$self->message_history->{$mask}->{channels}->{$channel}{validated} = 0;
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if($channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
|
||||
$channel_data->{validated} &= ~$self->{NICKSERV_VALIDATED};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$self->message_history->{$mask}->{channels}->{$channel}{validated} = 1 unless $do_not_validate;
|
||||
unless($do_not_validate) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if(not $channel_data->{validated} & $self->{NICKSERV_VALIDATED}) {
|
||||
$channel_data->{validated} |= $self->{NICKSERV_VALIDATED};
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub check_nickserv_accounts {
|
||||
my ($self, $nick, $account, $hostmask) = @_;
|
||||
my $force_validation = 0;
|
||||
my $message_account;
|
||||
|
||||
$self->{pbot}->logger->log("Checking nickserv accounts for nick $nick with account $account and hostmask " . (defined $hostmask ? $hostmask : 'undef') . "\n");
|
||||
|
||||
$account = lc $account;
|
||||
|
||||
if(not defined $hostmask) {
|
||||
foreach my $mask (keys %{ $self->{message_history} }) {
|
||||
if(exists $self->{message_history}->{$mask}->{nickserv_accounts} and exists $self->{message_history}->{$mask}->{nickserv_accounts}->{$account}) {
|
||||
# pre-existing mask found using this account previously
|
||||
#$self->{pbot}->logger->log("anti-flood: [check-account] $nick [nickserv: $account] seen previously as $mask.\n");
|
||||
$hostmask = $mask;
|
||||
}
|
||||
else {
|
||||
# no nickserv account set yet
|
||||
if($mask =~ m/^\Q$nick\E!/i) {
|
||||
# nick matches, must belong to account
|
||||
$hostmask = $mask;
|
||||
last;
|
||||
}
|
||||
($message_account, $hostmask) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($nick);
|
||||
|
||||
if(not defined $message_account) {
|
||||
$self->{pbot}->logger->log("No message account found for nick $nick.\n");
|
||||
($message_account) = $self->{pbot}->{messagehistory}->{database}->find_message_accounts_by_nickserv($account);
|
||||
|
||||
if(not $message_account) {
|
||||
$self->{pbot}->logger->log("No message account found for nickserv $account.\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
($message_account) = $self->{pbot}->{messagehistory}->{database}->find_message_accounts_by_mask($hostmask);
|
||||
if(not $message_account) {
|
||||
$self->{pbot}->logger->log("No message account found for hostmask $hostmask.\n");
|
||||
return;
|
||||
}
|
||||
$force_validation = 1;
|
||||
}
|
||||
|
||||
if(not defined $hostmask) {
|
||||
# could not find mask for nick/account
|
||||
$self->{pbot}->logger->log("anti-flood: [check-account] could not find mask for $nick [nickserv: $account]\n");
|
||||
return;
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("anti-flood: $hostmask: setting nickserv account to [$account]\n");
|
||||
$self->{message_history}->{$hostmask}->{nickserv_accounts}->{$account} = gettimeofday;
|
||||
$self->{message_history}->{$hostmask}->{nickserv_account} = $account;
|
||||
$self->{pbot}->logger->log("anti-flood: $message_account: setting nickserv account to [$account]\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_nickserv_account($message_account, $account, scalar gettimeofday);
|
||||
$self->{pbot}->{messagehistory}->{database}->set_current_nickserv_account($message_account, $account);
|
||||
|
||||
# check to see if any channels need check-ban validation
|
||||
foreach my $channel (keys %{ $self->message_history->{$hostmask}->{channels} }) {
|
||||
if($force_validation or exists $self->message_history->{$hostmask}->{channels}{$channel}->{needs_validation}) {
|
||||
# $self->{pbot}->logger->log("anti-flood: [check-account] $nick [nickserv: $account] needs check-ban validation for $hostmask in $channel.\n");
|
||||
$self->check_bans($hostmask, $channel);
|
||||
$hostmask = $self->{pbot}->{messagehistory}->{database}->find_most_recent_hostmask($message_account);
|
||||
my @channels = $self->{pbot}->{messagehistory}->{database}->get_channels($message_account);
|
||||
foreach my $channel (@channels) {
|
||||
my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated');
|
||||
if($force_validation or $channel_data->{validated} & $self->{NEEDS_CHECKBAN}) {
|
||||
$self->{pbot}->logger->log("anti-flood: [check-account] $nick [nickserv: $account] needs check-ban validation for $hostmask in $channel.\n");
|
||||
$self->check_bans($message_account, $hostmask, $channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -765,27 +644,32 @@ sub on_whoisaccount {
|
||||
$self->check_nickserv_accounts($nick, $account);
|
||||
}
|
||||
|
||||
sub save_message_history {
|
||||
my $self = shift;
|
||||
$self->{pbot}->logger->log("Saving message history\n");
|
||||
store($self->{message_history}, $self->{pbot}->{message_history_file});
|
||||
$self->{pbot}->logger->log("Message history saved\n");
|
||||
}
|
||||
|
||||
sub load_message_history {
|
||||
sub adjust_offenses {
|
||||
my $self = shift;
|
||||
|
||||
if(-e $self->{pbot}->{message_history_file}) {
|
||||
$self->{message_history} = retrieve($self->{pbot}->{message_history_file});
|
||||
} else {
|
||||
$self->{message_history} = {};
|
||||
$self->{pbot}->logger->log("Adjusting offenses . . .\n");
|
||||
|
||||
# decrease offenses counter if 24 hours have elapsed since latest offense
|
||||
my $channel_datas = $self->{pbot}->{messagehistory}->{database}->get_channel_datas_where_last_offense_older_than(gettimeofday - 60 * 60 * 24);
|
||||
foreach my $channel_data (@$channel_datas) {
|
||||
if($channel_data->{offenses} > 0) {
|
||||
my $id = delete $channel_data->{id};
|
||||
my $channel = delete $channel_data->{channel};
|
||||
$channel_data->{offenses}--;
|
||||
$channel_data->{last_offense} = gettimeofday;
|
||||
$self->{pbot}->logger->log("[adjust-offenses] [$id][$channel] 24 hours since last offense/decrease -- decreasing offenses to $channel_data->{offenses}\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($id, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
|
||||
$channel_datas = $self->{pbot}->{messagehistory}->{database}->get_channel_datas_with_enter_abuses();
|
||||
foreach my $channel_data (@$channel_datas) {
|
||||
my $id = delete $channel_data->{id};
|
||||
my $channel = delete $channel_data->{channel};
|
||||
$channel_data->{enter_abuses}--;
|
||||
$self->{pbot}->logger->log("[adjust-offenses] [$id][$channel] decreasing enter abuse offenses to $channel_data->{enter_abuses}\n");
|
||||
$self->{pbot}->{messagehistory}->{database}->update_channel_data($id, $channel, $channel_data);
|
||||
}
|
||||
}
|
||||
|
||||
sub save_message_history_cmd {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
$self->save_message_history;
|
||||
return "Message history saved.";
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -137,9 +137,11 @@ sub join_channel {
|
||||
my $self = shift;
|
||||
my ($from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
# FIXME -- update %channels hash?
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host made me join $arguments\n");
|
||||
$self->{pbot}->conn->join($arguments);
|
||||
foreach my $channel (split /\s+/, $arguments) {
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host made me join $channel\n");
|
||||
$self->{pbot}->conn->join($channel);
|
||||
}
|
||||
|
||||
return "/msg $nick Joining $arguments";
|
||||
}
|
||||
|
||||
@ -147,9 +149,13 @@ sub part_channel {
|
||||
my $self = shift;
|
||||
my ($from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
# FIXME -- update %channels hash?
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host made me part $arguments\n");
|
||||
$self->{pbot}->conn->part($arguments);
|
||||
$arguments = $from if not $arguments;
|
||||
|
||||
foreach my $channel (split /\s+/, $arguments) {
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host made me part $channel\n");
|
||||
$self->{pbot}->conn->part($channel);
|
||||
}
|
||||
|
||||
return "/msg $nick Parting $arguments";
|
||||
}
|
||||
|
||||
@ -157,10 +163,13 @@ sub ack_die {
|
||||
my $self = shift;
|
||||
my ($from, $nick, $user, $host, $arguments) = @_;
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host made me exit.\n");
|
||||
|
||||
# TODO: move all of those to an registerable atexit handler
|
||||
$self->{pbot}->factoids->save_factoids;
|
||||
$self->{pbot}->ignorelist->save_ignores;
|
||||
$self->{pbot}->antiflood->save_message_history;
|
||||
$self->{pbot}->{quotegrabs}->{quotegrabs_db}->end();
|
||||
$self->{pbot}->{quotegrabs}->{database}->end();
|
||||
$self->{pbot}->{messagehistory}->{database}->end();
|
||||
|
||||
$self->{pbot}->conn->privmsg($from, "Good-bye.") if defined $from;
|
||||
$self->{pbot}->conn->quit("Departure requested.");
|
||||
exit 0;
|
||||
|
@ -183,7 +183,9 @@ sub on_join {
|
||||
my ($self, $conn, $event) = @_;
|
||||
my ($nick, $user, $host, $channel) = ($event->nick, $event->user, $event->host, $event->to);
|
||||
|
||||
$self->{pbot}->antiflood->check_flood($channel, $nick, $user, $host, "JOIN", 4, 60 * 30, $self->{pbot}->antiflood->{FLOOD_JOIN});
|
||||
my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host);
|
||||
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, "JOIN", $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
$self->{pbot}->antiflood->check_flood($channel, $nick, $user, $host, "JOIN", 4, 60 * 30, $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
}
|
||||
|
||||
sub on_departure {
|
||||
@ -193,7 +195,19 @@ sub on_departure {
|
||||
my $text = uc $event->type;
|
||||
$text .= " $args";
|
||||
|
||||
$self->{pbot}->antiflood->check_flood($channel, $nick, $user, $host, $text, 4, 60 * 30, $self->{pbot}->antiflood->{FLOOD_JOIN});
|
||||
my $message_account = $self->{pbot}->{messagehistory}->get_message_account($nick, $user, $host);
|
||||
|
||||
if($text =~ m/^QUIT/) {
|
||||
# QUIT messages must be dispatched to each channel the user is on
|
||||
my @channels = $self->{pbot}->{messagehistory}->{database}->get_channels($message_account);
|
||||
foreach my $chan (@channels) {
|
||||
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $chan, $text, $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
}
|
||||
} else {
|
||||
$self->{pbot}->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $channel, $text, $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
}
|
||||
|
||||
$self->{pbot}->antiflood->check_flood($channel, $nick, $user, $host, $text, 4, 60 * 30, $self->{pbot}->{messagehistory}->{MSG_JOIN});
|
||||
|
||||
my $admin = $self->{pbot}->admins->find_admin($channel, "$nick!$user\@$host");
|
||||
if(defined $admin and $admin->{loggedin}) {
|
||||
|
@ -97,7 +97,10 @@ sub process_line {
|
||||
|
||||
my $pbot = $self->pbot;
|
||||
|
||||
$pbot->antiflood->check_flood($from, $nick, $user, $host, $text, $pbot->{MAX_FLOOD_MESSAGES}, 10, $pbot->antiflood->{FLOOD_CHAT}) if defined $from;
|
||||
my $message_account = $pbot->{messagehistory}->get_message_account($nick, $user, $host);
|
||||
$pbot->{messagehistory}->add_message($message_account, "$nick!$user\@$host", $from, $text, $pbot->{messagehistory}->{MSG_CHAT});
|
||||
|
||||
$pbot->antiflood->check_flood($from, $nick, $user, $host, $text, $pbot->{MAX_FLOOD_MESSAGES}, 10, $pbot->{messagehistory}->{MSG_CHAT}) if defined $from;
|
||||
|
||||
$text =~ s/^\s+//;
|
||||
$text =~ s/\s+$//;
|
||||
|
131
PBot/MessageHistory.pm
Normal file
131
PBot/MessageHistory.pm
Normal file
@ -0,0 +1,131 @@
|
||||
# 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;
|
||||
|
||||
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}->{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;
|
||||
$self->{MSG_JOIN} = 1;
|
||||
|
||||
$self->{pbot}->commands->register(sub { $self->recall_message(@_) }, "recall", 0);
|
||||
}
|
||||
|
||||
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 recall_message {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
if(not defined $from) {
|
||||
$self->{pbot}->logger->log("Command missing ~from parameter!\n");
|
||||
return "";
|
||||
}
|
||||
|
||||
if(not defined $arguments or not length $arguments) {
|
||||
return "Usage: recall <nick> [history [channel]] -- where [history] is an optional argument that is either an integral number of recent messages or a regex (without whitespace) of the text within the message; e.g., to recall the 3rd most recent message for nick, use `recall nick 3` or to recall a message containing 'pizza', use `recall nick pizza`; and [channel] is an optional channel, so you can use it from /msg (you will need to also specify [history] in this case)";
|
||||
}
|
||||
|
||||
$arguments = lc $arguments;
|
||||
|
||||
my @recalls = split /\s\+\s/, $arguments;
|
||||
|
||||
my ($recall_nick, $recall_history, $channel, $recall_nicks, $recall_text);
|
||||
|
||||
foreach my $recall (@recalls) {
|
||||
($recall_nick, $recall_history, $channel) = split(/\s+/, $recall, 3);
|
||||
|
||||
$recall_history = $nick eq $recall_nick ? 2 : 1 if not defined $recall_history; # skip recall command if recalling self without arguments
|
||||
$channel = $from if not defined $channel;
|
||||
|
||||
my ($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.";
|
||||
}
|
||||
|
||||
my $message;
|
||||
|
||||
if($recall_history =~ /^\d+$/) {
|
||||
# integral history
|
||||
my $max_messages = $self->{database}->get_max_messages($account, $channel);
|
||||
if($recall_history < 1 || $recall_history > $max_messages) {
|
||||
return "Please choose a history between 1 and $max_messages";
|
||||
}
|
||||
|
||||
$recall_history--;
|
||||
|
||||
$message = $self->{database}->recall_message_by_count($account, $channel, $recall_history, 'recall');
|
||||
} else {
|
||||
# regex history
|
||||
$message = $self->{database}->recall_message_by_text($account, $channel, $recall_history, 'recall');
|
||||
|
||||
if(not defined $message) {
|
||||
return "No such message for nick $found_nick in channel $channel containing text '$recall_history'";
|
||||
}
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("$nick ($from) recalled <$recall_nick/$channel> $message->{msg}\n");
|
||||
|
||||
my $text = $message->{msg};
|
||||
my $ago = ago(gettimeofday - $message->{timestamp});
|
||||
|
||||
if(not defined $recall_text) {
|
||||
if($text =~ s/^\/me\s+//) {
|
||||
$recall_text = "[$ago] * $found_nick $text";
|
||||
} else {
|
||||
$recall_text = "[$ago] <$found_nick> $text";
|
||||
}
|
||||
} else {
|
||||
if($text =~ s/^\/me\s+//) {
|
||||
$recall_text .= " [$ago] * $found_nick $text";
|
||||
} else {
|
||||
$recall_text .= " [$ago] <$found_nick> $text";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $recall_text;
|
||||
}
|
||||
|
||||
1;
|
645
PBot/MessageHistory_SQLite.pm
Normal file
645
PBot/MessageHistory_SQLite.pm
Normal file
@ -0,0 +1,645 @@
|
||||
# File: MessageHistory_SQLite.pm
|
||||
# Author: pragma_
|
||||
#
|
||||
# Purpose: SQLite backend for storing/retreiving a user's message history
|
||||
|
||||
package PBot::MessageHistory_SQLite;
|
||||
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
use DBI;
|
||||
use Carp qw(shortmess);
|
||||
|
||||
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 in " . __FILE__);
|
||||
$self->{filename} = delete $conf{filename} // $self->{pbot}->{data_dir} . '/message_history.sqlite3';
|
||||
|
||||
$self->{pbot}->timer->register(sub { $self->commit_message_history }, 5);
|
||||
$self->{new_entries} = 0;
|
||||
}
|
||||
|
||||
sub begin {
|
||||
my $self = shift;
|
||||
|
||||
$self->{pbot}->logger->log("Opening message history SQLite database: $self->{filename}\n");
|
||||
|
||||
$self->{dbh} = DBI->connect("dbi:SQLite:dbname=$self->{filename}", "", "", { RaiseError => 1, PrintError => 0 }) or die $DBI::errstr;
|
||||
|
||||
eval {
|
||||
#$self->{dbh}->trace($self->{dbh}->parse_trace_flags('SQL|1|test'));
|
||||
|
||||
$self->{dbh}->do(<<SQL);
|
||||
CREATE TABLE IF NOT EXISTS Hostmasks (
|
||||
hostmask TEXT PRIMARY KEY UNIQUE,
|
||||
id INTEGER,
|
||||
last_seen NUMERIC
|
||||
)
|
||||
SQL
|
||||
|
||||
$self->{dbh}->do(<<SQL);
|
||||
CREATE TABLE IF NOT EXISTS Accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
hostmask TEXT UNIQUE,
|
||||
nickserv TEXT
|
||||
)
|
||||
SQL
|
||||
|
||||
$self->{dbh}->do(<<SQL);
|
||||
CREATE TABLE IF NOT EXISTS Nickserv (
|
||||
id INTEGER,
|
||||
nickserv TEXT,
|
||||
timestamp NUMERIC
|
||||
)
|
||||
SQL
|
||||
|
||||
$self->{dbh}->do(<<SQL);
|
||||
CREATE TABLE IF NOT EXISTS Channels (
|
||||
id INTEGER,
|
||||
channel TEXT,
|
||||
enter_abuse INTEGER,
|
||||
enter_abuses INTEGER,
|
||||
offenses INTEGER,
|
||||
last_offense NUMERIC,
|
||||
last_seen NUMERIC,
|
||||
validated INTEGER,
|
||||
join_watch INTEGER
|
||||
)
|
||||
SQL
|
||||
|
||||
$self->{dbh}->do(<<SQL);
|
||||
CREATE TABLE IF NOT EXISTS Messages (
|
||||
id INTEGER,
|
||||
channel TEXT,
|
||||
msg TEXT,
|
||||
timestamp NUMERIC,
|
||||
mode INTEGER
|
||||
)
|
||||
SQL
|
||||
|
||||
$self->{dbh}->begin_work();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub end {
|
||||
my $self = shift;
|
||||
|
||||
$self->{pbot}->logger->log("Closing message history SQLite database\n");
|
||||
|
||||
if(exists $self->{dbh} and defined $self->{dbh}) {
|
||||
$self->{dbh}->commit() if $self->{new_entries};
|
||||
$self->{dbh}->disconnect();
|
||||
delete $self->{dbh};
|
||||
}
|
||||
}
|
||||
|
||||
sub get_nickserv_accounts {
|
||||
my ($self, $id) = @_;
|
||||
|
||||
my $nickserv_accounts = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT nickserv FROM Nickserv WHERE ID = ?');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return map {$_->[0]} @$nickserv_accounts;
|
||||
}
|
||||
|
||||
sub set_current_nickserv_account {
|
||||
my ($self, $id, $nickserv) = @_;
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('UPDATE Accounts SET nickserv = ? WHERE id = ?');
|
||||
$sth->bind_param(1, $nickserv);
|
||||
$sth->bind_param(2, $id);
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub get_current_nickserv_account {
|
||||
my ($self, $id) = @_;
|
||||
|
||||
my $nickserv = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT nickserv FROM Accounts WHERE id = ?');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->execute();
|
||||
return $sth->fetchrow_hashref()->{'nickserv'};
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $nickserv;
|
||||
}
|
||||
|
||||
sub create_nickserv {
|
||||
my ($self, $id, $nickserv) = @_;
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('INSERT INTO Nickserv SELECT ?, ?, 0 WHERE NOT EXISTS (SELECT 1 FROM Nickserv WHERE id = ? AND nickserv = ?)');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $nickserv);
|
||||
$sth->bind_param(3, $id);
|
||||
$sth->bind_param(4, $nickserv);
|
||||
my $rv = $sth->execute();
|
||||
$self->{new_entries}++ if $sth->rows;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub update_nickserv_account {
|
||||
my ($self, $id, $nickserv, $timestamp) = @_;
|
||||
|
||||
$self->{pbot}->logger->log("Updating nickserv account for id $id to $nickserv with timestamp [$timestamp]\n");
|
||||
|
||||
$self->create_nickserv($id, $nickserv);
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('UPDATE Nickserv SET timestamp = ? WHERE id = ? AND nickserv = ?');
|
||||
$sth->bind_param(1, $timestamp);
|
||||
$sth->bind_param(2, $id);
|
||||
$sth->bind_param(3, $nickserv);
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub add_message_account {
|
||||
my ($self, $mask, $link_id) = @_;
|
||||
my $id;
|
||||
|
||||
if(defined $link_id) {
|
||||
$id = $link_id;
|
||||
} else {
|
||||
$id = $self->get_new_account_id();
|
||||
}
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('INSERT INTO Hostmasks VALUES (?, ?, 0)');
|
||||
$sth->bind_param(1, $mask);
|
||||
$sth->bind_param(2, $id);
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
|
||||
if(not defined $link_id) {
|
||||
$sth = $self->{dbh}->prepare('INSERT INTO Accounts VALUES (?, ?, ?)');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $mask);
|
||||
$sth->bind_param(3, "");
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
}
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $id;
|
||||
}
|
||||
|
||||
sub find_message_account_by_nick {
|
||||
my ($self, $nick) = @_;
|
||||
|
||||
my ($id, $hostmask) = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id,hostmask FROM Hostmasks WHERE hostmask LIKE ? LIMIT 1');
|
||||
$sth->bind_param(1, "$nick!%");
|
||||
$sth->execute();
|
||||
my $row = $sth->fetchrow_hashref();
|
||||
return ($row->{id}, $row->{hostmask});
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
$hostmask =~ s/!.*$// if defined $hostmask;
|
||||
return ($id, $hostmask);
|
||||
}
|
||||
|
||||
sub find_message_accounts_by_nickserv {
|
||||
my ($self, $nickserv) = @_;
|
||||
|
||||
my $accounts = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id FROM Nickserv WHERE nickserv = ?');
|
||||
$sth->bind_param(1, $nickserv);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return map {$_->[0]} @$accounts;
|
||||
}
|
||||
|
||||
sub find_message_accounts_by_mask {
|
||||
my ($self, $mask) = @_;
|
||||
|
||||
$mask =~ s/\*/%/g;
|
||||
$mask =~ s/\?/_/g;
|
||||
$mask =~ s/\$.*$//;
|
||||
|
||||
my $accounts = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id FROM Hostmasks WHERE hostmask LIKE ?');
|
||||
$sth->bind_param(1, $mask);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return map {$_->[0]} @$accounts;
|
||||
}
|
||||
|
||||
sub get_message_account {
|
||||
my ($self, $nick, $user, $host) = @_;
|
||||
|
||||
my $mask = "$nick!$user\@$host";
|
||||
my $id = $self->get_message_account_id($mask);
|
||||
return $id if defined $id;
|
||||
|
||||
my $rows = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id,hostmask FROM Hostmasks WHERE hostmask LIKE ?');
|
||||
$sth->bind_param(1, "$nick!%");
|
||||
$sth->execute();
|
||||
my $rows = $sth->fetchall_arrayref({});
|
||||
|
||||
foreach my $row (@$rows) {
|
||||
$self->{pbot}->logger->log("Found matching nick $row->{hostmask} with id $row->{id}\n");
|
||||
}
|
||||
|
||||
if(not defined $rows->[0]) {
|
||||
$sth->bind_param(1, "%!$user\@$host");
|
||||
$sth->execute();
|
||||
$rows = $sth->fetchall_arrayref({});
|
||||
|
||||
foreach my $row (@$rows) {
|
||||
$self->{pbot}->logger->log("Found matching user\@host mask $row->{hostmask} with id $row->{id}\n");
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
|
||||
if(defined $rows->[0]) {
|
||||
$self->{pbot}->logger->log("message-history: [get-account] $nick!$user\@$host linked to $rows->[0]->{hostmask} with id $rows->[0]->{id}\n");
|
||||
$self->add_message_account("$nick!$user\@$host", $rows->[0]->{id});
|
||||
$self->devalidate_all_channels($rows->[0]->{id});
|
||||
my @nickserv_accounts = $self->get_nickserv_accounts($rows->[0]->{id});
|
||||
foreach my $nickserv_account (@nickserv_accounts) {
|
||||
$self->{pbot}->logger->log("$nick!$user\@$host [$rows->[0]->{id}] seen with nickserv account [$nickserv_account]\n");
|
||||
$self->{pbot}->antiflood->check_nickserv_accounts($nick, $nickserv_account, "$nick!$user\@$host");
|
||||
}
|
||||
return $rows->[0]->{id};
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("No account found for mask [$mask], adding new account\n");
|
||||
return $self->add_message_account($mask);
|
||||
}
|
||||
|
||||
sub find_most_recent_hostmask {
|
||||
my ($self, $id) = @_;
|
||||
|
||||
my $hostmask = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT hostmask FROM Hostmasks WHERE ID = ? ORDER BY last_seen DESC LIMIT 1');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->execute();
|
||||
return $sth->fetchrow_hashref()->{'hostmask'};
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $hostmask;
|
||||
}
|
||||
|
||||
sub update_hostmask_data {
|
||||
my ($self, $mask, $data) = @_;
|
||||
|
||||
eval {
|
||||
my $sql = 'UPDATE Hostmasks SET ';
|
||||
|
||||
my $comma = '';
|
||||
foreach my $key (keys %$data) {
|
||||
$sql .= "$comma$key = ?";
|
||||
$comma = ', ';
|
||||
}
|
||||
|
||||
$sql .= ' WHERE hostmask LIKE ?';
|
||||
|
||||
my $sth = $self->{dbh}->prepare($sql);
|
||||
|
||||
my $param = 1;
|
||||
foreach my $key (keys %$data) {
|
||||
$sth->bind_param($param++, $data->{$key});
|
||||
}
|
||||
|
||||
$sth->bind_param($param, $mask);
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub get_hostmasks_for_channel {
|
||||
my ($self, $channel) = @_;
|
||||
|
||||
my $hostmasks = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT hostmasks.id, hostmask FROM Hostmasks, Channels WHERE channels.id = hostmasks.id AND channel = ?');
|
||||
$sth->bind_param(1, $channel);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $hostmasks;
|
||||
}
|
||||
|
||||
sub add_message {
|
||||
my ($self, $id, $mask, $channel, $message) = @_;
|
||||
|
||||
$self->{pbot}->logger->log("Adding message [$id][$mask][$channel][$message->{msg}][$message->{timestamp}][$message->{mode}]\n");
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('INSERT INTO Messages VALUES (?, ?, ?, ?, ?)');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->bind_param(3, $message->{msg});
|
||||
$sth->bind_param(4, $message->{timestamp});
|
||||
$sth->bind_param(5, $message->{mode});
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
$self->update_channel_data($id, $channel, { last_seen => $message->{timestamp} });
|
||||
$self->update_hostmask_data($mask, { last_seen => $message->{timestamp} });
|
||||
}
|
||||
|
||||
sub get_recent_messages {
|
||||
my ($self, $id, $channel, $limit, $mode) = @_;
|
||||
$limit = 25 if not defined $limit;
|
||||
|
||||
my $mode_query = '';
|
||||
$mode_query = "AND mode = $mode" if defined $mode;
|
||||
|
||||
my $messages = eval {
|
||||
my $sth = $self->{dbh}->prepare(<<SQL);
|
||||
SELECT msg, mode, timestamp
|
||||
FROM Messages
|
||||
WHERE id = ? AND channel = ? $mode_query
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT ? OFFSET (SELECT COUNT(*) FROM Messages WHERE id = ? AND channel = ? $mode_query) - ?
|
||||
SQL
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->bind_param(3, $limit);
|
||||
$sth->bind_param(4, $id);
|
||||
$sth->bind_param(5, $channel);
|
||||
$sth->bind_param(6, $limit);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $messages;
|
||||
}
|
||||
|
||||
sub recall_message_by_count {
|
||||
my ($self, $id, $channel, $count, $ignore_command) = @_;
|
||||
|
||||
my $messages = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT msg, mode, timestamp FROM Messages WHERE id = ? AND channel = ? ORDER BY timestamp DESC LIMIT 10 OFFSET ?');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->bind_param(3, $count);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
|
||||
if(defined $ignore_command) {
|
||||
foreach my $message (@$messages) {
|
||||
next if $message->{msg} =~ m/^$self->{pbot}->{botnick}. $ignore_command/ or $message->{msg} =~ m/^$self->{pbot}->{trigger}$ignore_command/;
|
||||
return $message;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
return $messages->[0];
|
||||
}
|
||||
|
||||
sub recall_message_by_text {
|
||||
my ($self, $id, $channel, $text, $ignore_command) = @_;
|
||||
|
||||
my $messages = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT msg,mode,timestamp FROM Messages WHERE id = ? AND channel = ? AND msg LIKE ? ORDER BY timestamp DESC LIMIT 10');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->bind_param(3, "%$text%");
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
|
||||
if(defined $ignore_command) {
|
||||
foreach my $message (@$messages) {
|
||||
next if $message->{msg} =~ m/^$self->{pbot}->{botnick}. $ignore_command/ or $message->{msg} =~ m/^$self->{pbot}->{trigger}$ignore_command/;
|
||||
return $message;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
return $messages->[0];
|
||||
}
|
||||
|
||||
sub get_max_messages {
|
||||
my ($self, $id, $channel) = @_;
|
||||
|
||||
my $count = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT COUNT(*) FROM Messages WHERE id = ? AND channel = ?');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->execute();
|
||||
my $row = $sth->fetchrow_hashref();
|
||||
$sth->finish();
|
||||
return $row->{'COUNT(*)'};
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
$count = 0 if not defined $count;
|
||||
return $count;
|
||||
}
|
||||
|
||||
sub create_channel {
|
||||
my ($self, $id, $channel) = @_;
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare('INSERT INTO Channels SELECT ?, ?, 0, 0, 0, 0, 0, 0, 0 WHERE NOT EXISTS (SELECT 1 FROM Channels WHERE id = ? AND channel = ?)');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->bind_param(3, $id);
|
||||
$sth->bind_param(4, $channel);
|
||||
my $rv = $sth->execute();
|
||||
$self->{new_entries}++ if $sth->rows;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub get_channels {
|
||||
my ($self, $id) = @_;
|
||||
|
||||
my $channels = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT channel FROM Channels WHERE id = ?');
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return map {$_->[0]} @$channels;
|
||||
}
|
||||
|
||||
sub get_channel_data {
|
||||
my ($self, $id, $channel, @columns) = @_;
|
||||
|
||||
$self->create_channel($id, $channel);
|
||||
|
||||
my $channel_data = eval {
|
||||
my $sql = 'SELECT ';
|
||||
|
||||
if(not @columns) {
|
||||
$sql .= '*';
|
||||
} else {
|
||||
my $comma = '';
|
||||
foreach my $column (@columns) {
|
||||
$sql .= "$comma$column";
|
||||
$comma = ', ';
|
||||
}
|
||||
}
|
||||
|
||||
$sql .= ' FROM Channels WHERE id = ? AND channel = ?';
|
||||
my $sth = $self->{dbh}->prepare($sql);
|
||||
$sth->bind_param(1, $id);
|
||||
$sth->bind_param(2, $channel);
|
||||
$sth->execute();
|
||||
return $sth->fetchrow_hashref();
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $channel_data;
|
||||
}
|
||||
|
||||
sub update_channel_data {
|
||||
my ($self, $id, $channel, $data) = @_;
|
||||
|
||||
$self->create_channel($id, $channel);
|
||||
|
||||
eval {
|
||||
my $sql = 'UPDATE Channels SET ';
|
||||
|
||||
my $comma = '';
|
||||
foreach my $key (keys %$data) {
|
||||
$sql .= "$comma$key = ?";
|
||||
$comma = ', ';
|
||||
}
|
||||
|
||||
$sql .= ' WHERE id = ? AND channel = ?';
|
||||
|
||||
my $sth = $self->{dbh}->prepare($sql);
|
||||
|
||||
my $param = 1;
|
||||
foreach my $key (keys %$data) {
|
||||
$sth->bind_param($param++, $data->{$key});
|
||||
}
|
||||
|
||||
$sth->bind_param($param++, $id);
|
||||
$sth->bind_param($param, $channel);
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
sub get_channel_datas_where_last_offense_older_than {
|
||||
my ($self, $timestamp) = @_;
|
||||
|
||||
my $channel_datas = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id, channel, offenses, last_offense FROM Channels WHERE last_offense > 0 AND last_offense <= ?');
|
||||
$sth->bind_param(1, $timestamp);
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $channel_datas;
|
||||
}
|
||||
|
||||
sub get_channel_datas_with_enter_abuses {
|
||||
my ($self) = @_;
|
||||
|
||||
my $channel_datas = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id, channel, enter_abuses FROM Channels WHERE enter_abuses > 0');
|
||||
$sth->execute();
|
||||
return $sth->fetchall_arrayref({});
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return $channel_datas;
|
||||
}
|
||||
|
||||
sub devalidate_all_channels {
|
||||
my ($self, $id) = @_;
|
||||
|
||||
my $where = '';
|
||||
$where = 'WHERE id = ?' if defined $id;
|
||||
|
||||
eval {
|
||||
my $sth = $self->{dbh}->prepare("UPDATE Channels SET validated = 0 $where");
|
||||
$sth->bind_param(1, $id) if defined $id;
|
||||
$sth->execute();
|
||||
$self->{new_entries}++;
|
||||
};
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
}
|
||||
|
||||
# End of public API, the remaining are internal support routines for this module
|
||||
|
||||
sub get_new_account_id {
|
||||
my $self = shift;
|
||||
|
||||
my $id = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id FROM Accounts ORDER BY id DESC LIMIT 1');
|
||||
$sth->execute();
|
||||
my $row = $sth->fetchrow_hashref();
|
||||
return $row->{id};
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
return ++$id;
|
||||
}
|
||||
|
||||
sub get_message_account_id {
|
||||
my ($self, $mask) = @_;
|
||||
|
||||
my $id = eval {
|
||||
my $sth = $self->{dbh}->prepare('SELECT id FROM Hostmasks WHERE hostmask == ?');
|
||||
$sth->bind_param(1, $mask);
|
||||
$sth->execute();
|
||||
my $row = $sth->fetchrow_hashref();
|
||||
return $row->{id};
|
||||
};
|
||||
|
||||
$self->{pbot}->logger->log($@) if $@;
|
||||
$self->{pbot}->logger->log("get_message_account_id: returning id [". (defined $id ? $id: 'undef') . "] for mask [$mask]\n");
|
||||
return $id;
|
||||
}
|
||||
|
||||
sub commit_message_history {
|
||||
my $self = shift;
|
||||
|
||||
if($self->{new_entries} > 0) {
|
||||
$self->{pbot}->logger->log("Commiting $self->{new_entries} messages to SQLite\n");
|
||||
$self->{new_entries} = 0;
|
||||
$self->{dbh}->commit();
|
||||
$self->{dbh}->begin_work();
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
@ -31,6 +31,7 @@ use PBot::Channels;
|
||||
use PBot::BanTracker;
|
||||
|
||||
use PBot::LagChecker;
|
||||
use PBot::MessageHistory;
|
||||
use PBot::AntiFlood;
|
||||
|
||||
use PBot::Interpreter;
|
||||
@ -87,10 +88,10 @@ sub initialize {
|
||||
$self->{max_msg_len} = delete $conf{max_msg_len} // 425;
|
||||
$self->{MAX_FLOOD_MESSAGES} = delete $conf{MAX_FLOOD_MESSAGES} // 4;
|
||||
$self->{MAX_NICK_MESSAGES} = delete $conf{MAX_NICK_MESSAGES} // 32;
|
||||
$self->{message_history_file} = delete $conf{message_history_file} // "$ENV{HOME}/pbot/data/message_history";
|
||||
|
||||
$self->{trigger} = delete $conf{trigger} // '!';
|
||||
|
||||
my $messagehistory_file = delete $conf{message_history_file};
|
||||
my $channels_file = delete $conf{channels_file};
|
||||
my $admins_file = delete $conf{admins_file};
|
||||
my $ignorelist_file = delete $conf{ignorelist_file};
|
||||
@ -127,8 +128,9 @@ sub initialize {
|
||||
|
||||
$self->{bantracker} = PBot::BanTracker->new(pbot => $self);
|
||||
|
||||
$self->{lagchecker} = PBot::LagChecker->new(pbot => $self);
|
||||
$self->{antiflood} = PBot::AntiFlood->new(pbot => $self);
|
||||
$self->{lagchecker} = PBot::LagChecker->new(pbot => $self);
|
||||
$self->{messagehistory} = PBot::MessageHistory->new(pbot => $self, filename => $messagehistory_file);
|
||||
$self->{antiflood} = PBot::AntiFlood->new(pbot => $self);
|
||||
|
||||
$self->{ignorelist} = PBot::IgnoreList->new(pbot => $self, filename => $ignorelist_file);
|
||||
$self->{ignorelist}->load_ignores() if defined $ignorelist_file;
|
||||
|
@ -41,9 +41,9 @@ sub initialize {
|
||||
$self->{export_path} = delete $conf{export_path};
|
||||
$self->{export_site} = delete $conf{export_site};
|
||||
|
||||
$self->{quotegrabs_db} = PBot::Quotegrabs_SQLite->new(pbot => $self->{pbot}, filename => $self->{filename});
|
||||
#$self->{quotegrabs_db} = PBot::Quotegrabs_Hashtable->new(pbot => $self->{pbot}, filename => $self->{filename});
|
||||
$self->{quotegrabs_db}->begin();
|
||||
$self->{database} = PBot::Quotegrabs_SQLite->new(pbot => $self->{pbot}, filename => $self->{filename});
|
||||
#$self->{database} = PBot::Quotegrabs_Hashtable->new(pbot => $self->{pbot}, filename => $self->{filename});
|
||||
$self->{database}->begin();
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# The following could be in QuotegrabsCommands.pm, or they could be kept in here?
|
||||
@ -52,9 +52,6 @@ sub initialize {
|
||||
$self->{pbot}->commands->register(sub { $self->show_quotegrab(@_) }, "getq", 0);
|
||||
$self->{pbot}->commands->register(sub { $self->delete_quotegrab(@_) }, "delq", 0);
|
||||
$self->{pbot}->commands->register(sub { $self->show_random_quotegrab(@_) }, "rq", 0);
|
||||
|
||||
# ought to be in MessageTracker.pm once we create that module
|
||||
$self->{pbot}->commands->register(sub { $self->recall_message(@_) }, "recall", 0);
|
||||
}
|
||||
|
||||
sub uniq { my %seen; grep !$seen{$_}++, @_ }
|
||||
@ -63,7 +60,7 @@ sub export_quotegrabs {
|
||||
my $self = shift;
|
||||
return "Not enabled" if not defined $self->{export_path};
|
||||
|
||||
my $quotegrabs = $self->{quotegrabs_db}->get_all_quotegrabs();
|
||||
my $quotegrabs = $self->{database}->get_all_quotegrabs();
|
||||
|
||||
my $text;
|
||||
my $table_id = 1;
|
||||
@ -177,80 +174,43 @@ sub grab_quotegrab {
|
||||
foreach my $grab (@grabs) {
|
||||
($grab_nick, $grab_history, $channel) = split(/\s+/, $grab, 3);
|
||||
|
||||
if(not defined $grab_history) {
|
||||
$grab_history = $nick eq $grab_nick ? 2 : 1;
|
||||
}
|
||||
$grab_history = $nick eq $grab_nick ? 2 : 1 if not defined $grab_history; # skip grab command if grabbing self without arguments
|
||||
$channel = $from if not defined $channel;
|
||||
|
||||
if($grab_history =~ /^\d+$/ and ($grab_history < 1 || $grab_history > $self->{pbot}->{MAX_NICK_MESSAGES})) {
|
||||
return "/msg $nick Please choose a history between 1 and $self->{pbot}->{MAX_NICK_MESSAGES}";
|
||||
}
|
||||
|
||||
if(not $channel =~ m/^#/) {
|
||||
return "'$channel' is not a valid channel; usage: grab <nick> [[history] channel] (you must specify a history parameter before the channel parameter)";
|
||||
}
|
||||
|
||||
my $found_mask = undef;
|
||||
my $last_spoken = 0;
|
||||
foreach my $mask (keys %{ $self->{pbot}->antiflood->message_history }) {
|
||||
if($mask =~ m/^\Q$grab_nick\E!/i) {
|
||||
if(defined $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken}
|
||||
and $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken} > $last_spoken) {
|
||||
$last_spoken = $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken};
|
||||
$found_mask = $mask;
|
||||
}
|
||||
}
|
||||
my ($account, $found_nick) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($grab_nick);
|
||||
|
||||
if(not defined $account) {
|
||||
return "I don't know anybody named $grab_nick";
|
||||
}
|
||||
|
||||
if(not defined $found_mask) {
|
||||
return "No message history for $grab_nick in channel $channel. Usage: grab <nick> [history [channel]]; to specify channel, you must also specify history";
|
||||
}
|
||||
$grab_nick = $found_nick; # convert nick to proper casing
|
||||
|
||||
($grab_nick) = $found_mask =~ m/^([^!]+)!/; # convert $grab_nick to match casing of nick
|
||||
|
||||
if(not exists $self->{pbot}->antiflood->message_history->{$found_mask}->{channels}->{$channel}) {
|
||||
return "No message history for $grab_nick in channel $channel. Usage: grab <nick> [history [channel]]; to specify channel, you must also specify history";
|
||||
}
|
||||
|
||||
my @messages = @{ $self->{pbot}->antiflood->message_history->{$found_mask}->{channels}->{$channel}{messages} };
|
||||
my $message;
|
||||
|
||||
if($grab_history =~ /^\d+$/) {
|
||||
# integral history
|
||||
$grab_history--;
|
||||
|
||||
if($grab_history > $#messages) {
|
||||
return "$grab_nick has only " . ($#messages + 1) . " messages in the history for channel $channel.";
|
||||
my $max_messages = $self->{pbot}->{messagehistory}->{database}->get_max_messages($account, $channel);
|
||||
if($grab_history < 1 || $grab_history > $max_messages) {
|
||||
return "Please choose a history between 1 and $max_messages";
|
||||
}
|
||||
|
||||
$grab_history = $#messages - $grab_history;
|
||||
$grab_history--;
|
||||
|
||||
$message = $self->{pbot}->{messagehistory}->{database}->recall_message_by_count($account, $channel, $grab_history, 'grab');
|
||||
} else {
|
||||
# regex history
|
||||
my $ret = eval {
|
||||
my $i = $#messages;
|
||||
$i-- if($nick =~ /^\Q$grab_nick\E$/i); # skip 'grab' command if grabbing own nick
|
||||
my $found = 0;
|
||||
while($i >= 0) {
|
||||
if($messages[$i]->{msg} =~ m/$grab_history/i) {
|
||||
$grab_history = $i;
|
||||
$found = 1;
|
||||
last;
|
||||
}
|
||||
$i--;
|
||||
}
|
||||
$message = $self->{pbot}->{messagehistory}->{database}->recall_message_by_text($account, $channel, $grab_history, 'grab');
|
||||
|
||||
if($found == 0) {
|
||||
return "/msg $nick No message containing regex '$grab_history' found for $grab_nick in channel $channel.";
|
||||
} else {
|
||||
return undef;
|
||||
}
|
||||
};
|
||||
return "/msg $nick Bad grab regex: $@" if $@;
|
||||
if(defined $ret) {
|
||||
return $ret;
|
||||
if(not defined $message) {
|
||||
return "No such message for nick $grab_nick in channel $channel containing text '$grab_history'";
|
||||
}
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("$nick ($from) grabbed <$grab_nick/$channel> $messages[$grab_history]->{msg}\n");
|
||||
$self->{pbot}->logger->log("$nick ($from) grabbed <$grab_nick/$channel> $message->{msg}\n");
|
||||
|
||||
if(not defined $grab_nicks) {
|
||||
$grab_nicks = $grab_nick;
|
||||
@ -258,7 +218,7 @@ sub grab_quotegrab {
|
||||
$grab_nicks .= "+$grab_nick";
|
||||
}
|
||||
|
||||
my $text = $messages[$grab_history]->{msg};
|
||||
my $text = $message->{msg};
|
||||
|
||||
if(not defined $grab_text) {
|
||||
$grab_text = $text;
|
||||
@ -279,7 +239,7 @@ sub grab_quotegrab {
|
||||
$quotegrab->{text} = $grab_text;
|
||||
$quotegrab->{id} = undef;
|
||||
|
||||
$quotegrab->{id} = $self->{quotegrabs_db}->add_quotegrab($quotegrab);
|
||||
$quotegrab->{id} = $self->{database}->add_quotegrab($quotegrab);
|
||||
|
||||
if(not defined $quotegrab->{id}) {
|
||||
return "Failed to grab quote.";
|
||||
@ -300,7 +260,7 @@ sub grab_quotegrab {
|
||||
sub delete_quotegrab {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
my $quotegrab = $self->{quotegrabs_db}->get_quotegrab($arguments);
|
||||
my $quotegrab = $self->{database}->get_quotegrab($arguments);
|
||||
|
||||
if(not defined $quotegrab) {
|
||||
return "/msg $nick No quotegrab matching id $arguments found.";
|
||||
@ -310,7 +270,7 @@ sub delete_quotegrab {
|
||||
return "You are not the grabber of this quote.";
|
||||
}
|
||||
|
||||
$self->{quotegrabs_db}->delete_quotegrab($arguments);
|
||||
$self->{database}->delete_quotegrab($arguments);
|
||||
$self->export_quotegrabs();
|
||||
|
||||
my $text = $quotegrab->{text};
|
||||
@ -327,7 +287,7 @@ sub delete_quotegrab {
|
||||
sub show_quotegrab {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
my $quotegrab = $self->{quotegrabs_db}->get_quotegrab($arguments);
|
||||
my $quotegrab = $self->{database}->get_quotegrab($arguments);
|
||||
|
||||
if(not defined $quotegrab) {
|
||||
return "/msg $nick No quotegrab matching id $arguments found.";
|
||||
@ -387,7 +347,7 @@ sub show_random_quotegrab {
|
||||
|
||||
$channel_search = undef if defined $channel_search and $channel_search !~ /^#/;
|
||||
|
||||
my $quotegrab = $self->{quotegrabs_db}->get_random_quotegrab($nick_search, $channel_search, $text_search);
|
||||
my $quotegrab = $self->{database}->get_random_quotegrab($nick_search, $channel_search, $text_search);
|
||||
|
||||
if(not defined $quotegrab) {
|
||||
my $result = "No quotes grabbed ";
|
||||
@ -417,117 +377,4 @@ sub show_random_quotegrab {
|
||||
}
|
||||
}
|
||||
|
||||
# this ought to be in MessageTracker.pm once we create that module
|
||||
sub recall_message {
|
||||
my ($self, $from, $nick, $user, $host, $arguments) = @_;
|
||||
|
||||
if(not defined $from) {
|
||||
$self->{pbot}->logger->log("Command missing ~from parameter!\n");
|
||||
return "";
|
||||
}
|
||||
|
||||
if(not defined $arguments or not length $arguments) {
|
||||
return "Usage: recall <nick> [history [channel]] -- where [history] is an optional argument that is either an integral number of recent messages or a regex (without whitespace) of the text within the message; e.g., to recall the 3rd most recent message for nick, use `recall nick 3` or to recall a message containing 'pizza', use `recall nick pizza`; and [channel] is an optional channel, so you can use it from /msg (you will need to also specify [history] in this case)";
|
||||
}
|
||||
|
||||
$arguments = lc $arguments;
|
||||
|
||||
my @recalls = split /\s\+\s/, $arguments;
|
||||
|
||||
my ($recall_nick, $recall_history, $channel, $recall_nicks, $recall_text);
|
||||
|
||||
foreach my $recall (@recalls) {
|
||||
($recall_nick, $recall_history, $channel) = split(/\s+/, $recall, 3);
|
||||
|
||||
if(not defined $recall_history) {
|
||||
$recall_history = $nick eq $recall_nick ? 2 : 1;
|
||||
}
|
||||
$channel = $from if not defined $channel;
|
||||
|
||||
if($recall_history =~ /^\d+$/ and ($recall_history < 1 || $recall_history > $self->{pbot}->{MAX_NICK_MESSAGES})) {
|
||||
return "/msg $nick Please choose a history between 1 and $self->{pbot}->{MAX_NICK_MESSAGES}";
|
||||
}
|
||||
|
||||
my $found_mask = undef;
|
||||
my $last_spoken = 0;
|
||||
foreach my $mask (keys %{ $self->{pbot}->antiflood->message_history }) {
|
||||
if($mask =~ m/^\Q$recall_nick\E!/i) {
|
||||
if(defined $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken}
|
||||
and $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken} > $last_spoken) {
|
||||
$last_spoken = $self->{pbot}->antiflood->message_history->{$mask}->{channels}->{$channel}{last_spoken};
|
||||
$found_mask = $mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(not defined $found_mask) {
|
||||
return "No message history for $recall_nick in channel $channel. Usage: recall <nick> [history [channel]]; to specify channel, you must also specify history";
|
||||
}
|
||||
|
||||
if(not exists $self->{pbot}->antiflood->message_history->{$found_mask}->{channels}->{$channel}) {
|
||||
return "No message history for $recall_nick in channel $channel. Usage: recall <nick> [history [channel]]; to specify channel, you must also specify history";
|
||||
}
|
||||
|
||||
my @messages = @{ $self->{pbot}->antiflood->message_history->{$found_mask}->{channels}->{$channel}{messages} };
|
||||
my ($found_nick) = $found_mask =~ m/^([^!]+)/;
|
||||
|
||||
if($recall_history =~ /^\d+$/) {
|
||||
# integral history
|
||||
$recall_history--;
|
||||
|
||||
if($recall_history > $#messages) {
|
||||
return "$recall_nick has only " . ($#messages + 1) . " messages in the history for channel $channel.";
|
||||
}
|
||||
|
||||
$recall_history = $#messages - $recall_history;
|
||||
} else {
|
||||
# regex history
|
||||
my $ret = eval {
|
||||
my $i = $#messages;
|
||||
$i-- if($nick =~ /^\Q$recall_nick\E$/i); # skip 'recall' command if recallbing own nick
|
||||
my $found = 0;
|
||||
while($i >= 0) {
|
||||
if($messages[$i]->{msg} =~ m/$recall_history/i) {
|
||||
$recall_history = $i;
|
||||
$found = 1;
|
||||
last;
|
||||
}
|
||||
$i--;
|
||||
}
|
||||
|
||||
if($found == 0) {
|
||||
return "/msg $nick No message containing regex '$recall_history' found for $recall_nick in channel $channel.";
|
||||
} else {
|
||||
return undef;
|
||||
}
|
||||
};
|
||||
return "/msg $nick Bad recall regex: $@" if $@;
|
||||
if(defined $ret) {
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
$self->{pbot}->logger->log("$nick ($from) recalled <$recall_nick/$channel> $messages[$recall_history]->{msg}\n");
|
||||
|
||||
my $text = $messages[$recall_history]->{msg};
|
||||
my $ago = ago(gettimeofday - $messages[$recall_history]->{timestamp});
|
||||
|
||||
if(not defined $recall_text) {
|
||||
if($text =~ s/^\/me\s+//) {
|
||||
$recall_text = "[$ago] * $found_nick $text";
|
||||
} else {
|
||||
$recall_text = "[$ago] <$found_nick> $text";
|
||||
}
|
||||
} else {
|
||||
if($text =~ s/^\/me\s+//) {
|
||||
$recall_text .= " [$ago] * $found_nick $text";
|
||||
} else {
|
||||
$recall_text .= " [$ago] <$found_nick> $text";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $recall_text;
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -63,6 +63,7 @@ sub end {
|
||||
|
||||
if(exists $self->{dbh} and defined $self->{dbh}) {
|
||||
$self->{dbh}->disconnect();
|
||||
delete $self->{dbh};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,8 @@ use warnings;
|
||||
# These are set automatically by the build/commit script
|
||||
use constant {
|
||||
BUILD_NAME => "PBot",
|
||||
BUILD_REVISION => 568,
|
||||
BUILD_DATE => "2014-05-08",
|
||||
BUILD_REVISION => 569,
|
||||
BUILD_DATE => "2014-05-13",
|
||||
};
|
||||
|
||||
1;
|
||||
|
2
pbot.pl
2
pbot.pl
@ -110,7 +110,7 @@ $config{factoids_file} = "$config{data_dir}/factoids";
|
||||
$config{quotegrabs_file} = "$config{data_dir}/quotegrabs.sqlite3";
|
||||
|
||||
# Location of file containing message history
|
||||
$config{message_history_file} = "$config{data_dir}/message_history";
|
||||
$config{message_history_file} = "$config{data_dir}/message_history.sqlite3";
|
||||
|
||||
# Create and initialize bot object
|
||||
my $pbot = PBot::PBot->new(%config);
|
||||
|
Loading…
Reference in New Issue
Block a user