3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-12-27 13:12:42 +01:00
pbot/lib/PBot/MessageHistory_SQLite.pm

1940 lines
69 KiB
Perl
Raw Normal View History

# File: MessageHistory_SQLite.pm
#
2021-06-19 06:23:34 +02:00
# Purpose: SQLite backend for storing/retreiving a user's message history.
# Peforms intelligent hostmask and nickserv heuristics to link nicknames
# in order to ensure message history is stored in the right user account
# ids. This is also extremely useful for detecting ban-evasions and listing
# also-known-as data for a nickname (see the !aka bot command).
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
package PBot::MessageHistory_SQLite;
use parent 'PBot::Class';
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2019-07-11 03:40:53 +02:00
use DBI;
use Carp qw/shortmess/;
use Time::HiRes qw/time/;
2016-02-10 16:10:37 +01:00
use Text::CSV;
use Text::Levenshtein qw/fastdistance/;
2017-08-02 06:36:31 +02:00
use Time::Duration;
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
$self->{filename} = $conf{filename} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/message_history.sqlite3';
$self->{new_entries} = 0;
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'debug_link', 0);
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'debug_aka', 0);
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'sqlite_commit_interval', 30);
$self->{pbot}->{registry}->add_default('text', 'messagehistory', 'sqlite_debug', $conf{sqlite_debug} // 0);
$self->{pbot}->{registry}->add_trigger('messagehistory', 'sqlite_commit_interval', sub { $self->sqlite_commit_interval_trigger(@_) });
$self->{pbot}->{registry}->add_trigger('messagehistory', 'sqlite_debug', sub { $self->sqlite_debug_trigger(@_) });
$self->{pbot}->{event_queue}->enqueue(
2020-02-15 23:38:32 +01:00
sub { $self->commit_message_history },
$self->{pbot}->{registry}->get_value('messagehistory', 'sqlite_commit_interval'),
'messagehistory commit'
2020-02-15 23:38:32 +01:00
);
$self->{alias_type}->{WEAK} = 0;
$self->{alias_type}->{STRONG} = 1;
}
sub sqlite_commit_interval_trigger {
2020-02-15 23:38:32 +01:00
my ($self, $section, $item, $newvalue) = @_;
$self->{pbot}->{event_queue}->update_interval('messagehistory commit', $newvalue);
}
sub sqlite_debug_trigger {
2020-02-15 23:38:32 +01:00
my ($self, $section, $item, $newvalue) = @_;
2019-06-26 18:34:19 +02:00
if ($newvalue) {
open $self->{trace_layer}, '>:via(PBot::SQLiteLoggerLayer)', PBot::SQLiteLogger->new(pbot => $self->{pbot});
} else {
close $self->{trace_layer} if $self->{trace_layer};
delete $self->{trace_layer};
}
$self->{dbh}->trace($self->{dbh}->parse_trace_flags("SQL|$newvalue")) if defined $self->{dbh};
}
sub begin {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Opening message history SQLite database: $self->{filename}\n");
2020-02-15 23:38:32 +01:00
$self->{dbh} = DBI->connect("dbi:SQLite:dbname=$self->{filename}", "", "", {RaiseError => 1, PrintError => 0, AutoInactiveDestroy => 1, sqlite_unicode => 1})
or die $DBI::errstr;
2020-02-15 23:38:32 +01:00
eval {
my $sqlite_debug = $self->{pbot}->{registry}->get_value('messagehistory', 'sqlite_debug');
use PBot::SQLiteLoggerLayer;
use PBot::SQLiteLogger;
if ($sqlite_debug) {
open $self->{trace_layer}, '>:via(PBot::SQLiteLoggerLayer)', PBot::SQLiteLogger->new(pbot => $self->{pbot});
$self->{dbh}->trace($self->{dbh}->parse_trace_flags("SQL|$sqlite_debug"), $self->{trace_layer});
}
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Hostmasks (
hostmask TEXT PRIMARY KEY UNIQUE COLLATE NOCASE,
id INTEGER,
last_seen NUMERIC,
nickchange INTEGER,
nick TEXT COLLATE NOCASE,
user TEXT COLLATE NOCASE,
host TEXT COLLATE NOCASE
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Accounts (
id INTEGER PRIMARY KEY,
hostmask TEXT UNIQUE COLLATE NOCASE,
nickserv TEXT COLLATE NOCASE
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Nickserv (
2019-06-26 18:34:19 +02:00
id INTEGER,
nickserv TEXT COLLATE NOCASE,
2017-06-12 08:33:38 +02:00
timestamp NUMERIC,
UNIQUE (id, nickserv)
)
2016-02-10 12:42:42 +01:00
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
2016-02-10 12:42:42 +01:00
CREATE TABLE IF NOT EXISTS Gecos (
id INTEGER,
gecos TEXT COLLATE NOCASE,
2017-06-12 08:33:38 +02:00
timestamp NUMERIC,
UNIQUE (id, gecos)
2016-02-10 12:42:42 +01:00
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Channels (
id INTEGER,
channel TEXT COLLATE NOCASE,
enter_abuse INTEGER,
enter_abuses INTEGER,
offenses INTEGER,
last_offense NUMERIC,
last_seen NUMERIC,
validated INTEGER,
2016-01-17 01:55:48 +01:00
join_watch INTEGER,
2017-06-12 08:33:38 +02:00
unbanmes INTEGER,
UNIQUE (id, channel)
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Messages (
id INTEGER,
channel TEXT COLLATE NOCASE,
msg TEXT COLLATE NOCASE,
timestamp NUMERIC,
mode INTEGER,
hostmask TEXT COLLATE NOCASE
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do(<<SQL);
CREATE TABLE IF NOT EXISTS Aliases (
id INTEGER,
alias INTEGER,
type INTEGER
)
SQL
2020-02-15 23:38:32 +01:00
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS MsgIdx1 ON Messages(id, channel, mode)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS AliasIdx1 ON Aliases(id, alias, type)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS AliasIdx2 ON Aliases(alias, id, type)');
2020-02-15 23:38:32 +01:00
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS hostmask_nick_idx on Hostmasks (nick)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS hostmask_host_idx on Hostmasks (host)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS hostmasks_id_idx on Hostmasks (id)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS gecos_id_idx on Gecos (id)');
$self->{dbh}->do('CREATE INDEX IF NOT EXISTS nickserv_id_idx on Nickserv (id)');
2020-02-15 23:38:32 +01:00
$self->{dbh}->begin_work();
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub end {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Closing message history SQLite database\n");
2020-02-15 23:38:32 +01:00
if (exists $self->{dbh} and defined $self->{dbh}) {
$self->{dbh}->commit() if $self->{new_entries};
$self->{dbh}->disconnect();
close $self->{trace_layer} if $self->{trace_layer};
2020-02-15 23:38:32 +01:00
delete $self->{dbh};
}
}
2016-02-10 12:42:42 +01:00
sub get_gecos {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
my $gecos = eval {
my $sth = $self->{dbh}->prepare('SELECT gecos FROM Gecos WHERE ID = ?');
$sth->execute($id);
return $sth->fetchall_arrayref();
};
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$gecos;
2016-02-10 12:42:42 +01:00
}
sub get_nickserv_accounts {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
my $nickserv_accounts = eval {
my $sth = $self->{dbh}->prepare('SELECT nickserv FROM Nickserv WHERE ID = ?');
$sth->execute($id);
return $sth->fetchall_arrayref();
};
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$nickserv_accounts;
}
sub set_current_nickserv_account {
2020-02-15 23:38:32 +01:00
my ($self, $id, $nickserv) = @_;
eval {
my $sth = $self->{dbh}->prepare('UPDATE Accounts SET nickserv = ? WHERE id = ?');
$sth->execute($nickserv, $id);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub get_current_nickserv_account {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
my $nickserv = eval {
my $sth = $self->{dbh}->prepare('SELECT nickserv FROM Accounts WHERE id = ?');
$sth->execute($id);
my $row = $sth->fetchrow_hashref();
if (defined $row) { return $row->{'nickserv'}; }
else { return undef; }
};
$self->{pbot}->{logger}->log($@) if $@;
return $nickserv;
}
sub create_nickserv {
2020-02-15 23:38:32 +01:00
my ($self, $id, $nickserv) = @_;
eval {
my $sth = $self->{dbh}->prepare('INSERT OR IGNORE INTO Nickserv VALUES (?, ?, 0)');
my $rv = $sth->execute($id, $nickserv);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub update_nickserv_account {
2020-02-15 23:38:32 +01:00
my ($self, $id, $nickserv, $timestamp) = @_;
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
#$self->{pbot}->{logger}->log("Updating nickserv account for id $id to $nickserv with timestamp [$timestamp]\n");
2020-02-15 23:38:32 +01:00
$self->create_nickserv($id, $nickserv);
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare('UPDATE Nickserv SET timestamp = ? WHERE id = ? AND nickserv = ?');
$sth->execute($timestamp, $id, $nickserv);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
2016-02-10 12:42:42 +01:00
sub create_gecos {
2020-02-15 23:38:32 +01:00
my ($self, $id, $gecos) = @_;
eval {
my $sth = $self->{dbh}->prepare('INSERT OR IGNORE INTO Gecos VALUES (?, ?, 0)');
my $rv = $sth->execute($id, $gecos);
$self->{new_entries}++ if $sth->rows;
};
$self->{pbot}->{logger}->log($@) if $@;
2016-02-10 12:42:42 +01:00
}
sub update_gecos {
2020-02-15 23:38:32 +01:00
my ($self, $id, $gecos, $timestamp) = @_;
2016-02-10 12:42:42 +01:00
2020-02-15 23:38:32 +01:00
$self->create_gecos($id, $gecos);
2016-02-10 12:42:42 +01:00
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare('UPDATE Gecos SET timestamp = ? WHERE id = ? AND gecos = ?');
$sth->execute($timestamp, $id, $gecos);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
2016-02-10 12:42:42 +01:00
}
sub add_message_account {
2020-02-15 23:38:32 +01:00
my ($self, $mask, $link_id, $link_type) = @_;
my $id;
my ($nick, $user, $host) = $mask =~ m/^([^!]+)!([^@]+)@(.*)/;
if (defined $link_id and $link_type == $self->{alias_type}->{STRONG}) { $id = $link_id; }
else {
$id = $self->get_new_account_id();
$self->{pbot}->{logger}->log("Got new account id $id\n");
}
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare('INSERT INTO Hostmasks VALUES (?, ?, ?, 0, ?, ?, ?)');
$sth->execute($mask, $id, scalar time, $nick, $user, $host);
2020-02-15 23:38:32 +01:00
$self->{new_entries}++;
if ((not defined $link_id) || ((defined $link_id) && ($link_type == $self->{alias_type}->{WEAK}))) {
$sth = $self->{dbh}->prepare('INSERT INTO Accounts VALUES (?, ?, ?)');
$sth->execute($id, $mask, "");
$self->{new_entries}++;
$self->{pbot}->{logger}->log("Added new account $id for mask $mask\n");
}
};
$self->{pbot}->{logger}->log($@) if $@;
2020-02-15 23:38:32 +01:00
if (defined $link_id && $link_type == $self->{alias_type}->{WEAK}) {
$self->{pbot}->{logger}->log("Weakly linking $id to $link_id\n");
$self->link_alias($id, $link_id, $link_type);
}
2020-02-15 23:38:32 +01:00
return $id;
}
sub find_message_account_by_id {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
2020-02-15 23:38:32 +01:00
my $hostmask = eval {
my $sth = $self->{dbh}->prepare('SELECT hostmask FROM Hostmasks WHERE id = ? ORDER BY last_seen DESC LIMIT 1');
$sth->execute($id);
my $row = $sth->fetchrow_hashref();
return $row->{hostmask};
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return $hostmask;
}
sub find_message_account_by_nick {
2020-02-15 23:38:32 +01:00
my ($self, $nick) = @_;
2020-02-15 23:38:32 +01:00
my ($id, $hostmask) = eval {
my $sth = $self->{dbh}->prepare('SELECT id, hostmask FROM Hostmasks WHERE nick = ? ORDER BY last_seen DESC LIMIT 1');
$sth->execute($nick);
my $row = $sth->fetchrow_hashref();
return ($row->{id}, $row->{hostmask});
};
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return ($id, $hostmask);
}
sub find_message_accounts_by_nickserv {
2020-02-15 23:38:32 +01:00
my ($self, $nickserv) = @_;
my $accounts = eval {
my $sth = $self->{dbh}->prepare('SELECT id FROM Nickserv WHERE nickserv = ?');
$sth->execute($nickserv);
return $sth->fetchall_arrayref();
};
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$accounts;
}
sub find_message_accounts_by_mask {
2020-02-15 23:38:32 +01:00
my ($self, $mask) = @_;
my $qmask = quotemeta $mask;
$qmask =~ s/_/\\_/g;
$qmask =~ s/\\\*/%/g;
$qmask =~ s/\\\?/_/g;
$qmask =~ s/\\\$.*$//;
my $accounts = eval {
my $sth = $self->{dbh}->prepare('SELECT id FROM Hostmasks WHERE hostmask LIKE ? ESCAPE "\"');
$sth->execute($qmask);
return $sth->fetchall_arrayref();
};
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$accounts;
}
sub get_message_account_ancestor {
2020-02-15 23:38:32 +01:00
my $self = shift;
my $id = $self->get_message_account(@_);
$id = $self->get_ancestor_id($id);
return $id;
}
sub get_message_account {
2020-02-15 23:38:32 +01:00
my ($self, $nick, $user, $host, $orig_nick) = @_;
2020-02-15 23:38:32 +01:00
($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host);
=cut
use Devel::StackTrace;
my $trace = Devel::StackTrace->new(indent => 1, ignore_class => ['PBot::PBot', 'PBot::IRC']);
$self->{pbot}->{logger}->log("get_message_account stacktrace: " . $trace->as_string() . "\n");
=cut
2020-02-15 23:38:32 +01:00
my $mask = "$nick!$user\@$host";
my $id = $self->get_message_account_id($mask);
return $id if defined $id;
$self->{pbot}->{logger}->log("Getting new message account for $nick!$user\@$host\n");
$self->{pbot}->{logger}->log("Nick-changing from $orig_nick\n") if defined $orig_nick;
2020-02-15 23:38:32 +01:00
my $do_nothing = 0;
my $sth;
2020-02-15 23:38:32 +01:00
my ($rows, $link_type) = eval {
my ($account1) = $host =~ m{/([^/]+)$};
$account1 //= '';
# extract ips from hosts like 75-42-36-105.foobar.com as 75.42.36.105
my $hostip;
2020-02-15 23:38:32 +01:00
if ($host =~ m/(\d+[[:punct:]]\d+[[:punct:]]\d+[[:punct:]]\d+)\D/) {
$hostip = $1;
$hostip =~ s/[[:punct:]]/./g;
2017-08-02 06:36:31 +02:00
}
# nick-change from $orig_nick to $nick
2020-02-15 23:38:32 +01:00
if (defined $orig_nick) {
# get original nick's account id
my $orig_id = $self->get_message_account_id("$orig_nick!$user\@$host");
2020-02-15 23:38:32 +01:00
# changing nick to a Guest
2020-02-15 23:38:32 +01:00
if ($nick =~ m/^Guest\d+$/) {
# find most recent *!user@host, if any
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE user = ? and host = ? ORDER BY last_seen DESC LIMIT 1');
2020-02-15 23:38:32 +01:00
$sth->execute($user, $host);
2020-02-15 23:38:32 +01:00
my $rows = $sth->fetchall_arrayref({});
# found a probable match
2020-02-15 23:38:32 +01:00
if (defined $rows->[0]) {
my $link_type = $self->{alias_type}->{STRONG};
# if 48 hours have elapsed since this *!user@host was seen
# then still link the Guest to this account, but weakly
if (time - $rows->[0]->{last_seen} > 60 * 60 * 48) {
2020-02-15 23:38:32 +01:00
$link_type = $self->{alias_type}->{WEAK};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log(
"Longer than 48 hours (" . concise duration(time - $rows->[0]->{last_seen}) . ")"
. " for $rows->[0]->{hostmask} for $nick!$user\@$host, degrading to weak link\n"
);
2020-02-15 23:38:32 +01:00
}
# log match and return link
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("6: nick-change guest match: $rows->[0]->{id}: $rows->[0]->{hostmask}\n");
$orig_nick = undef; # nick-change handled
2020-02-15 23:38:32 +01:00
return ($rows, $link_type);
}
}
# find all accounts that match nick!*@*, sorted by last-seen
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE nick = ? ORDER BY last_seen DESC');
$sth->execute($nick);
my $rows = $sth->fetchall_arrayref({});
# no nicks found, strongly link to original account
2020-02-15 23:38:32 +01:00
if (not defined $rows->[0]) {
$rows->[0] = { id => $orig_id, hostmask => "$orig_nick!$user\@$host" };
$orig_nick = undef; # nick-change handled
2020-02-15 23:38:32 +01:00
return ($rows, $self->{alias_type}->{STRONG});
}
# look up original nick's NickServ accounts outside of upcoming loop
my @orig_nickserv_accounts = $self->get_nickserv_accounts($orig_id);
# go over the list of nicks and see if any identifying details match
2020-02-15 23:38:32 +01:00
my %processed_nicks;
my %processed_akas;
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
$self->{pbot}->{logger}->log("Found matching nick-change account: [$row->{id}] $row->{hostmask}\n");
2020-02-15 23:38:32 +01:00
my ($tnick) = $row->{hostmask} =~ m/^([^!]+)!/;
# don't process duplicates
2020-02-15 23:38:32 +01:00
next if exists $processed_nicks{lc $tnick};
$processed_nicks{lc $tnick} = 1;
# get all akas for this nick
2020-02-15 23:38:32 +01:00
my %akas = $self->get_also_known_as($tnick);
# check each aka for identifying details
2020-02-15 23:38:32 +01:00
foreach my $aka (keys %akas) {
# skip dubious links
2020-02-15 23:38:32 +01:00
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
# don't process duplicates
2020-02-15 23:38:32 +01:00
next if exists $processed_akas{$akas{$aka}->{id}};
$processed_akas{$akas{$aka}->{id}} = 1;
$self->{pbot}->{logger}->log("Testing alias [$akas{$aka}->{id}] $aka\n");
my $match = 0;
# account ids or *!user@host matches
2020-02-15 23:38:32 +01:00
if ($akas{$aka}->{id} == $orig_id || $aka =~ m/^.*!\Q$user\E\@\Q$host\E$/i) {
$self->{pbot}->{logger}->log("1: match: $akas{$aka}->{id} vs $orig_id // $aka vs *!$user\@$host\n");
$match = 1;
goto MATCH;
}
# check if any nickserv accounts match
2020-02-15 23:38:32 +01:00
if (@orig_nickserv_accounts) {
my @nickserv_accounts = $self->get_nickserv_accounts($akas{$aka}->{id});
foreach my $ns1 (@orig_nickserv_accounts) {
foreach my $ns2 (@nickserv_accounts) {
if ($ns1 eq $ns2) {
$self->{pbot}->{logger}->log("Got matching nickserv: $ns1\n");
$match = 1;
goto MATCH;
}
}
}
}
# check if hosts match
2020-02-15 23:38:32 +01:00
my ($thost) = $aka =~ m/@(.*)$/;
if ($thost =~ m{/}) {
my ($account2) = $thost =~ m{/([^/]+)$};
if ($account1 ne $account2) {
$self->{pbot}->{logger}->log("Skipping non-matching cloaked hosts: $host vs $thost\n");
next;
} else {
$self->{pbot}->{logger}->log("Cloaked hosts match: $host vs $thost\n");
$rows->[0] = {
id => $self->get_ancestor_id($akas{$aka}->{id}),
hostmask => $aka,
};
2020-02-15 23:38:32 +01:00
return ($rows, $self->{alias_type}->{STRONG});
}
}
# fuzzy match hosts
2020-02-15 23:38:32 +01:00
my $distance = fastdistance($host, $thost);
my $length = (length($host) > length($thost)) ? length $host : length $thost;
#$self->{pbot}->{logger}->log("distance: " . ($distance / $length) . " -- $host vs $thost\n") if $length != 0;
if ($length != 0 && $distance / $length < 0.50) {
$self->{pbot}->{logger}->log("2: distance match: $host vs $thost == " . ($distance / $length) . "\n");
$match = 1;
} else {
# handle cases like 99.57.140.149 vs 99-57-140-149.lightspeed.sntcca.sbcglobal.net
if (defined $hostip) {
if ($hostip eq $thost) {
$match = 1;
$self->{pbot}->{logger}->log("3: IP vs hostname match: $host vs $thost\n");
}
} elsif ($thost =~ m/(\d+[[:punct:]]\d+[[:punct:]]\d+[[:punct:]]\d+)\D/) {
my $thostip = $1;
$thostip =~ s/[[:punct:]]/./g;
if ($thostip eq $host) {
$match = 1;
$self->{pbot}->{logger}->log("4: IP vs hostname match: $host vs $thost\n");
}
}
}
MATCH:
if ($match) {
$self->{pbot}->{logger}->log("Using this match.\n");
$rows->[0] = {id => $self->get_ancestor_id($akas{$aka}->{id}), hostmask => $aka};
return ($rows, $self->{alias_type}->{STRONG});
}
}
}
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Creating new nickchange account!\n");
2020-02-15 23:38:32 +01:00
my $new_id = $self->add_message_account($mask);
$self->link_alias($orig_id, $new_id, $self->{alias_type}->{WEAK});
$self->update_hostmask_data($mask, {nickchange => 1, last_seen => scalar time});
2020-02-15 23:38:32 +01:00
$do_nothing = 1;
$rows->[0] = {id => $new_id};
return ($rows, 0);
} # end nick-change
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
if ($host =~ m{^gateway/web/irccloud.com}) {
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE host = ? ORDER BY last_seen DESC');
$sth->execute("gateway/web/irccloud.com/x-$user");
my $rows = $sth->fetchall_arrayref({});
if (defined $rows->[0]) {
$self->{pbot}->{logger}->log("5: irccloud match: $rows->[0]->{id}: $rows->[0]->{hostmask}\n");
return ($rows, $self->{alias_type}->{STRONG});
}
}
2020-02-15 23:38:32 +01:00
if ($host =~ m{^nat/([^/]+)/}) {
my $nat = $1;
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE nick = ? AND host = ? ORDER BY last_seen DESC');
$sth->execute($nick, "nat/$nat/x-$user");
my $rows = $sth->fetchall_arrayref({});
if (defined $rows->[0]) {
$self->{pbot}->{logger}->log("6: nat match: $rows->[0]->{id}: $rows->[0]->{hostmask}\n");
return ($rows, $self->{alias_type}->{STRONG});
}
2017-08-02 06:36:31 +02:00
}
2020-02-15 23:38:32 +01:00
# cloaked hostmask
if ($host =~ m{/}) {
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE host = ? ORDER BY last_seen DESC');
$sth->execute($host);
my $rows = $sth->fetchall_arrayref({});
if (defined $rows->[0]) {
$self->{pbot}->{logger}->log("6: cloak match: $rows->[0]->{id}: $rows->[0]->{hostmask}\n");
return ($rows, $self->{alias_type}->{STRONG});
}
}
2020-02-15 23:38:32 +01:00
# guests
if ($nick =~ m/^Guest\d+$/) {
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE user = ? and host = ? ORDER BY last_seen DESC');
$sth->execute($user, $host);
my $rows = $sth->fetchall_arrayref({});
if (defined $rows->[0]) {
my $link_type = $self->{alias_type}->{STRONG};
if (time - $rows->[0]->{last_seen} > 60 * 60 * 48) {
2020-02-15 23:38:32 +01:00
$link_type = $self->{alias_type}->{WEAK};
$self->{pbot}->{logger}->log(
"Longer than 48 hours (" . concise duration(time - $rows->[0]->{last_seen}) . ") for $rows->[0]->{hostmask} for $nick!$user\@$host, degrading to weak link\n");
2020-02-15 23:38:32 +01:00
}
$self->{pbot}->{logger}->log("6: guest match: $rows->[0]->{id}: $rows->[0]->{hostmask}\n");
return ($rows, $link_type);
}
}
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE nick = ? ORDER BY last_seen DESC');
$sth->execute($nick);
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
my $link_type = $self->{alias_type}->{WEAK};
my %processed_nicks;
my %processed_akas;
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
$self->{pbot}->{logger}->log("Found matching nick $row->{hostmask} with id $row->{id}\n");
my ($tnick) = $row->{hostmask} =~ m/^([^!]+)!/;
2020-02-15 23:38:32 +01:00
next if exists $processed_nicks{lc $tnick};
$processed_nicks{lc $tnick} = 1;
2020-02-15 23:38:32 +01:00
my %akas = $self->get_also_known_as($tnick);
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
2020-02-15 23:38:32 +01:00
next if exists $processed_akas{$akas{$aka}->{id}};
$processed_akas{$akas{$aka}->{id}} = 1;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Testing alias [$akas{$aka}->{id}] $aka\n");
2020-02-15 23:38:32 +01:00
my ($thost) = $aka =~ m/@(.*)$/;
2020-02-15 23:38:32 +01:00
if ($thost =~ m{/}) {
my ($account2) = $thost =~ m{/([^/]+)$};
2020-02-15 23:38:32 +01:00
if ($account1 ne $account2) {
$self->{pbot}->{logger}->log("Skipping non-matching cloaked hosts: $host vs $thost\n");
next;
} else {
$self->{pbot}->{logger}->log("Cloaked hosts match: $host vs $thost\n");
$rows->[0] = {id => $self->get_ancestor_id($akas{$aka}->{id}), hostmask => $aka};
return ($rows, $self->{alias_type}->{STRONG});
}
}
2020-02-15 23:38:32 +01:00
my $distance = fastdistance($host, $thost);
my $length = (length($host) > length($thost)) ? length $host : length $thost;
#$self->{pbot}->{logger}->log("distance: " . ($distance / $length) . " -- $host vs $thost\n") if $length != 0;
my $match = 0;
if ($length != 0 && $distance / $length < 0.50) {
$self->{pbot}->{logger}->log("7: distance match: $host vs $thost == " . ($distance / $length) . "\n");
$match = 1;
} else {
# handle cases like 99.57.140.149 vs 99-57-140-149.lightspeed.sntcca.sbcglobal.net
if (defined $hostip) {
if ($hostip eq $thost) {
$match = 1;
$self->{pbot}->{logger}->log("8: IP vs hostname match: $host vs $thost\n");
}
} elsif ($thost =~ m/(\d+[[:punct:]]\d+[[:punct:]]\d+[[:punct:]]\d+)\D/) {
my $thostip = $1;
$thostip =~ s/[[:punct:]]/./g;
if ($thostip eq $host) {
$match = 1;
$self->{pbot}->{logger}->log("9: IP vs hostname match: $host vs $thost\n");
}
}
}
2020-02-15 23:38:32 +01:00
if ($match) {
$rows->[0] = {id => $self->get_ancestor_id($akas{$aka}->{id}), hostmask => $aka};
return ($rows, $self->{alias_type}->{STRONG});
}
}
}
2020-02-15 23:38:32 +01:00
if (not defined $rows->[0]) {
$link_type = $self->{alias_type}->{STRONG};
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT id, hostmask, last_seen FROM Hostmasks WHERE user = ? AND host = ? ORDER BY last_seen DESC');
$sth->execute($user, $host);
$rows = $sth->fetchall_arrayref({});
if (defined $rows->[0] and time - $rows->[0]->{last_seen} > 60 * 60 * 48) {
2020-02-15 23:38:32 +01:00
$link_type = $self->{alias_type}->{WEAK};
$self->{pbot}->{logger}->log(
"Longer than 48 hours (" . concise duration(time - $rows->[0]->{last_seen}) . ") for $rows->[0]->{hostmask} for $nick!$user\@$host, degrading to weak link\n");
2020-02-15 23:38:32 +01:00
}
=cut
foreach my $row (@$rows) {
$self->{pbot}->{logger}->log("Found matching user\@host mask $row->{hostmask} with id $row->{id}\n");
}
=cut
2020-02-15 23:38:32 +01:00
}
if (defined $rows->[0]) { $self->{pbot}->{logger}->log("10: matching *!user\@host: $rows->[0]->{id}: $rows->[0]->{hostmask}\n"); }
2020-02-15 23:38:32 +01:00
return ($rows, $link_type);
};
if (my $exception = $@) {
$self->{pbot}->{logger}->log("Exception getting account: $exception");
}
# nothing else to do here for nick-change, return id
2020-02-15 23:38:32 +01:00
return $rows->[0]->{id} if $do_nothing;
if (defined $rows->[0] and not defined $orig_nick) {
if ($link_type == $self->{alias_type}->{STRONG}) {
my $host1 = lc "$nick!$user\@$host";
my $host2 = lc $rows->[0]->{hostmask};
2020-02-15 23:38:32 +01:00
my ($nick1) = $host1 =~ m/^([^!]+)!/;
my ($nick2) = $host2 =~ m/^([^!]+)!/;
2020-02-15 23:38:32 +01:00
my $distance = fastdistance($nick1, $nick2);
my $length = (length $nick1 > length $nick2) ? length $nick1 : length $nick2;
my $irc_cloak = $self->{pbot}->{registry}->get_value('irc', 'cloak') // 'user';
if ($distance > 1 && ($nick1 !~ /^guest/ && $nick2 !~ /^guest/) && ($host1 !~ /$irc_cloak/ || $host2 !~ /$irc_cloak/)) {
2020-02-15 23:38:32 +01:00
my $id = $rows->[0]->{id};
$self->{pbot}->{logger}->log("[$nick1][$nick2] $distance / $length\n");
$self->{pbot}->{logger}->log("Possible bogus account: ($id) $host1 vs ($id) $host2\n");
}
}
$self->{pbot}->{logger}->log("message-history: [get-account] $nick!$user\@$host "
. ($link_type == $self->{alias_type}->{WEAK} ? "weakly linked to" : "added to account")
. " $rows->[0]->{hostmask} with id $rows->[0]->{id}\n"
);
2020-02-15 23:38:32 +01:00
$self->add_message_account("$nick!$user\@$host", $rows->[0]->{id}, $link_type);
$self->devalidate_all_channels($rows->[0]->{id});
$self->update_hostmask_data("$nick!$user\@$host", { last_seen => scalar time });
2020-02-15 23:38:32 +01:00
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");
}
2020-02-15 23:38:32 +01:00
return $rows->[0]->{id};
}
$self->{pbot}->{logger}->log("No account found for $mask, adding new account\n");
2020-02-15 23:38:32 +01:00
return $self->add_message_account($mask);
}
sub find_most_recent_hostmask {
2020-02-15 23:38:32 +01:00
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->execute($id);
return $sth->fetchrow_hashref()->{'hostmask'};
};
$self->{pbot}->{logger}->log($@) if $@;
return $hostmask;
}
sub update_hostmask_data {
2020-02-15 23:38:32 +01:00
my ($self, $mask, $data) = @_;
2020-02-15 23:38:32 +01:00
eval {
my $sql = 'UPDATE Hostmasks SET ';
2020-02-15 23:38:32 +01:00
my $comma = '';
foreach my $key (keys %$data) {
$sql .= "$comma$key = ?";
$comma = ', ';
}
2020-02-15 23:38:32 +01:00
$sql .= ' WHERE hostmask == ?';
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare($sql);
2020-02-15 23:38:32 +01:00
my $param = 1;
foreach my $key (keys %$data) { $sth->bind_param($param++, $data->{$key}); }
2020-02-15 23:38:32 +01:00
$sth->bind_param($param, $mask);
$sth->execute();
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub get_nickserv_accounts_for_hostmask {
2020-02-15 23:38:32 +01:00
my ($self, $hostmask) = @_;
2020-02-15 23:38:32 +01:00
my $nickservs = eval {
my $sth = $self->{dbh}->prepare('SELECT nickserv FROM Hostmasks, Nickserv WHERE nickserv.id = hostmasks.id AND hostmasks.hostmask = ?');
$sth->execute($hostmask);
return $sth->fetchall_arrayref();
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$nickservs;
}
2016-02-10 12:42:42 +01:00
sub get_gecos_for_hostmask {
2020-02-15 23:38:32 +01:00
my ($self, $hostmask) = @_;
2016-02-10 12:42:42 +01:00
2020-02-15 23:38:32 +01:00
my $gecos = eval {
my $sth = $self->{dbh}->prepare('SELECT gecos FROM Hostmasks, Gecos WHERE gecos.id = hostmasks.id AND hostmasks.hostmask = ?');
$sth->execute($hostmask);
return $sth->fetchall_arrayref();
};
2016-02-10 12:42:42 +01:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$gecos;
2016-02-10 12:42:42 +01:00
}
sub get_hostmasks_for_channel {
2020-02-15 23:38:32 +01:00
my ($self, $channel) = @_;
2020-02-15 23:38:32 +01:00
my $hostmasks = eval {
my $sth = $self->{dbh}->prepare('SELECT hostmasks.id, hostmask FROM Hostmasks, Channels WHERE channels.id = hostmasks.id AND channel = ?');
$sth->execute($channel);
return $sth->fetchall_arrayref({});
};
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return $hostmasks;
}
sub get_hostmasks_for_nickserv {
2020-02-15 23:38:32 +01:00
my ($self, $nickserv) = @_;
2020-02-15 23:38:32 +01:00
my $hostmasks = eval {
my $sth = $self->{dbh}->prepare('SELECT hostmasks.id, hostmask, nickserv FROM Hostmasks, Nickserv WHERE nickserv.id = hostmasks.id AND nickserv = ?');
$sth->execute($nickserv);
return $sth->fetchall_arrayref({});
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return $hostmasks;
}
sub add_message {
my ($self, $id, $hostmask, $channel, $message) = @_;
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare('INSERT INTO Messages VALUES (?, ?, ?, ?, ?, ?)');
$sth->execute($id, $channel, $message->{msg}, $message->{timestamp}, $message->{mode}, $hostmask);
2020-02-15 23:38:32 +01:00
$self->{new_entries}++;
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
$self->update_channel_data($id, $channel, { last_seen => $message->{timestamp }});
$self->update_hostmask_data($hostmask, { last_seen => $message->{timestamp }});
}
sub get_recent_messages {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, $limit, $mode, $nick) = @_;
$limit //= 25;
2020-02-15 23:38:32 +01:00
$channel = lc $channel;
2020-02-15 23:38:32 +01:00
my $mode_query = '';
$mode_query = "AND mode = $mode" if defined $mode;
2020-02-15 23:38:32 +01:00
my $messages = eval {
my $sql = "SELECT * FROM Messages WHERE ";
my %seen_id;
2020-02-15 23:38:32 +01:00
my %akas;
if (defined $mode and $mode == $self->{pbot}->{messagehistory}->{MSG_NICKCHANGE}) {
%akas = $self->get_also_known_as($nick);
} else {
$akas{$id} = {
id => $id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
2020-02-15 23:38:32 +01:00
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
2020-02-15 23:38:32 +01:00
}
my $ids = join " OR ", map { "id = ?" } keys %seen_id;
2020-02-15 23:38:32 +01:00
$sql .= "($ids) AND channel = ? $mode_query ORDER BY timestamp ASC LIMIT ? OFFSET (SELECT COUNT(*) FROM Messages WHERE ($ids) AND channel = ? $mode_query) - ?";
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare($sql);
2020-02-15 23:38:32 +01:00
my $param = 1;
map { $sth->bind_param($param++, $_) } keys %seen_id;
2020-02-15 23:38:32 +01:00
$sth->bind_param($param++, $channel);
$sth->bind_param($param++, $limit);
map { $sth->bind_param($param++, $_) } keys %seen_id;
2020-02-15 23:38:32 +01:00
$sth->bind_param($param++, $channel);
$sth->bind_param($param, $limit);
$sth->execute;
2020-02-15 23:38:32 +01:00
return $sth->fetchall_arrayref({});
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return $messages;
}
sub get_recent_messages_from_channel {
2020-02-15 23:38:32 +01:00
my ($self, $channel, $limit, $mode, $direction) = @_;
$limit //= 25;
$direction //= 'ASC';
2020-02-15 23:38:32 +01:00
$channel = lc $channel;
my $mode_query = '';
$mode_query = "AND mode = $mode" if defined $mode;
my $messages = eval {
my $sql = "SELECT * FROM Messages WHERE channel = ? $mode_query ORDER BY timestamp $direction LIMIT ?";
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare($sql);
$sth->execute($channel, $limit);
return $sth->fetchall_arrayref({});
};
$self->{pbot}->{logger}->log($@) if $@;
return $messages;
}
sub get_message_context {
my ($self, $message, $before, $after, $count, $text, $context_id, $context_nick) = @_;
my %seen_id;
my $ids = '';
my $sql = 'SELECT * FROM Messages WHERE channel = ? ';
if (defined $context_id) {
my %akas;
if (defined $context_nick) {
%akas = $self->get_also_known_as($context_nick);
} else {
$akas{$context_id} = {
id => $context_id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
}
$ids = join " OR ", map { "id = ?" } keys %seen_id;
$ids = "AND ($ids) ";
}
$sql .= $ids;
2020-02-15 23:38:32 +01:00
my ($messages_before, $messages_after, $messages_count);
if (defined $count and $count > 1) {
my $search = "%$text%";
$search =~ s/\*/%/g;
$search =~ s/\?/_/g;
2020-02-15 23:38:32 +01:00
$messages_count = eval {
$sql .= 'AND msg LIKE ? ESCAPE "\" AND timestamp < ? AND mode = 0 ORDER BY timestamp DESC LIMIT ?';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $message->{channel});
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->bind_param($param++, $search);
$sth->bind_param($param++, $message->{timestamp});
$sth->bind_param($param++, $count - 1);
$sth->execute;
2020-02-15 23:38:32 +01:00
return [reverse @{$sth->fetchall_arrayref({})}];
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
}
2020-02-15 23:38:32 +01:00
if (defined $before and $before > 0) {
$messages_before = eval {
$sql .= ' AND timestamp < ? AND mode = 0 ORDER BY timestamp DESC LIMIT ?';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $message->{channel});
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->bind_param($param++, $message->{timestamp});
$sth->bind_param($param++, $before);
$sth->execute;
2020-02-15 23:38:32 +01:00
return [reverse @{$sth->fetchall_arrayref({})}];
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
}
2015-06-16 02:58:25 +02:00
2020-02-15 23:38:32 +01:00
if (defined $after and $after > 0) {
$messages_after = eval {
$sql .= ' AND timestamp > ? AND mode = 0 ORDER BY timestamp ASC LIMIT ?';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $message->{channel});
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->bind_param($param++, $message->{timestamp});
$sth->bind_param($param++, $after);
$sth->execute;
2020-02-15 23:38:32 +01:00
return $sth->fetchall_arrayref({});
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
}
2015-06-16 02:58:25 +02:00
2020-02-15 23:38:32 +01:00
my @messages;
push(@messages, @$messages_before) if defined $messages_before;
push(@messages, @$messages_count) if defined $messages_count;
push(@messages, $message);
push(@messages, @$messages_after) if defined $messages_after;
return \@messages;
}
sub recall_message_by_count {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, $count, $ignore_command, $use_aliases) = @_;
my $messages = eval {
my $sql = 'SELECT * FROM Messages WHERE ';
my %seen_id;
if (defined $id) {
my %akas;
2020-02-15 23:38:32 +01:00
if (defined $use_aliases) {
%akas = $self->get_also_known_as($use_aliases);
} else {
$akas{$id} = {
id => $id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
2020-02-15 23:38:32 +01:00
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
}
my $ids = join " OR ", map { "id = ?" } keys %seen_id;
$sql .= "($ids) AND ";
}
$sql .= 'channel = ? ORDER BY timestamp DESC LIMIT 10 OFFSET ?';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->bind_param($param++, $channel);
$sth->bind_param($param++, $count);
$sth->execute;
return $sth->fetchall_arrayref({});
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
2020-02-15 23:38:32 +01:00
if (defined $ignore_command) {
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
my $bot_trigger = $self->{pbot}->{registry}->get_value('general', 'trigger');
foreach my $message (@$messages) {
next if $message->{msg} =~ m/^$botnick.? $ignore_command/ or $message->{msg} =~ m/^$bot_trigger$ignore_command/;
return $message;
}
2020-02-15 23:38:32 +01:00
return undef;
}
2020-02-15 23:38:32 +01:00
return $messages->[0];
}
sub recall_message_by_text {
my ($self, $id, $channel, $text, $ignore_command, $use_aliases) = @_;
2019-06-26 18:34:19 +02:00
my $search = "%$text%";
$search =~ s/\*/%/g;
$search =~ s/\?/_/g;
my $messages = eval {
my $sql = 'SELECT * FROM Messages WHERE channel = ? AND msg LIKE ? ESCAPE "\" ';
my %seen_id;
if (defined $id) {
my %akas;
if (defined $use_aliases) {
%akas = $self->get_also_known_as($use_aliases);
} else {
$akas{$id} = {
id => $id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
}
my $ids = join " OR ", map { "id = ?" } keys %seen_id;
$sql .= "AND ($ids) ";
}
$sql .= 'ORDER BY timestamp DESC LIMIT 10';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $channel);
$sth->bind_param($param++, $search);
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->execute;
return $sth->fetchall_arrayref({});
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
2020-02-15 23:38:32 +01:00
if (defined $ignore_command) {
my $bot_trigger = $self->{pbot}->{registry}->get_value('general', 'trigger');
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
foreach my $message (@$messages) {
next
if $message->{msg} =~ m/^$botnick.? $ignore_command/i
or $message->{msg} =~ m/^(?:\s*[^,:\(\)\+\*\/ ]+[,:]?\s+)?$bot_trigger$ignore_command/i
or $message->{msg} =~ m/^\s*$ignore_command.? $botnick$/i;
return $message;
}
2020-02-15 23:38:32 +01:00
return undef;
}
2020-02-15 23:38:32 +01:00
return $messages->[0];
}
sub get_random_message {
my ($self, $id, $channel, $use_aliases) = @_;
my $message = eval {
my $sql = 'SELECT * FROM Messages WHERE channel = ? AND mode = ? ';
my %seen_id;
if (defined $id) {
my %akas;
if (defined $use_aliases) {
%akas = $self->get_also_known_as($use_aliases);
} else {
$akas{$id} = {
id => $id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
}
my $ids = join " OR ", map { "id = ?" } keys %seen_id;
$sql .= "AND ($ids) ";
}
$sql .= 'ORDER BY RANDOM() LIMIT 1';
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $channel);
$sth->bind_param($param++, $self->{pbot}->{messagehistory}->{MSG_CHAT});
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->execute;
return $sth->fetchrow_hashref;
};
$self->{pbot}->{logger}->log($@) if $@;
return $message;
}
sub get_max_messages {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, $use_aliases) = @_;
2020-02-15 23:38:32 +01:00
my $count = eval {
my $sql = 'SELECT COUNT(*) FROM Messages WHERE channel = ? AND ';
2020-02-15 23:38:32 +01:00
my %akas;
if (defined $use_aliases) {
%akas = $self->get_also_known_as($use_aliases);
} else {
$akas{$id} = {
id => $id,
type => $self->{alias_type}->{STRONG},
nickchange => 0,
};
}
2020-02-15 23:38:32 +01:00
my %seen_id;
2020-02-15 23:38:32 +01:00
foreach my $aka (keys %akas) {
next if $akas{$aka}->{type} == $self->{alias_type}->{WEAK};
next if $akas{$aka}->{nickchange} == 1;
next if exists $seen_id{$akas{$aka}->{id}};
$seen_id{$akas{$aka}->{id}} = 1;
2020-02-15 23:38:32 +01:00
}
my $ids = join " OR ", map { "id = ?" } keys %seen_id;
2020-02-15 23:38:32 +01:00
$sql .= "($ids)";
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare($sql);
my $param = 1;
$sth->bind_param($param++, $channel);
map { $sth->bind_param($param++, $_) } keys %seen_id;
$sth->execute;
return $sth->fetchrow_hashref->{'COUNT(*)'};
2020-02-15 23:38:32 +01:00
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
$count = 0 if not defined $count;
return $count;
}
sub create_channel {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel) = @_;
eval {
my $sth = $self->{dbh}->prepare('INSERT OR IGNORE INTO Channels VALUES (?, ?, 0, 0, 0, 0, 0, 0, 0, 0)');
my $rv = $sth->execute($id, $channel);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub get_channels {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
my $channels = eval {
my $sth = $self->{dbh}->prepare('SELECT channel FROM Channels WHERE id = ?');
$sth->execute($id);
return $sth->fetchall_arrayref();
};
$self->{pbot}->{logger}->log($@) if $@;
return map { $_->[0] } @$channels;
}
sub get_channel_data {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, @columns) = @_;
2020-02-15 23:38:32 +01:00
$self->create_channel($id, $channel);
2020-02-15 23:38:32 +01:00
my $channel_data = eval {
my $sql = 'SELECT ';
2020-02-15 23:38:32 +01:00
if (not @columns) { $sql .= '*'; }
else {
my $comma = '';
foreach my $column (@columns) {
$sql .= "$comma$column";
$comma = ', ';
}
}
2020-02-15 23:38:32 +01:00
$sql .= ' FROM Channels WHERE id = ? AND channel = ?';
my $sth = $self->{dbh}->prepare($sql);
$sth->execute($id, $channel);
return $sth->fetchrow_hashref();
};
$self->{pbot}->{logger}->log($@) if $@;
return $channel_data;
}
sub update_channel_data {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, $data) = @_;
2020-02-15 23:38:32 +01:00
$self->create_channel($id, $channel);
2020-02-15 23:38:32 +01:00
eval {
my $sql = 'UPDATE Channels SET ';
2020-02-15 23:38:32 +01:00
my $comma = '';
foreach my $key (keys %$data) {
$sql .= "$comma$key = ?";
$comma = ', ';
}
2020-02-15 23:38:32 +01:00
$sql .= ' WHERE id = ? AND channel = ?';
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare($sql);
2020-02-15 23:38:32 +01:00
my $param = 1;
foreach my $key (keys %$data) { $sth->bind_param($param++, $data->{$key}); }
2020-02-15 23:38:32 +01:00
$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 {
2020-02-15 23:38:32 +01:00
my ($self, $timestamp) = @_;
my $channel_datas = eval {
my $sth = $self->{dbh}->prepare('SELECT id, channel, offenses, last_offense, unbanmes FROM Channels WHERE last_offense > 0 AND last_offense <= ?');
$sth->execute($timestamp);
return $sth->fetchall_arrayref({});
};
$self->{pbot}->{logger}->log($@) if $@;
return $channel_datas;
}
sub get_channel_datas_with_enter_abuses {
2020-02-15 23:38:32 +01:00
my ($self) = @_;
my $channel_datas = eval {
my $sth = $self->{dbh}->prepare('SELECT id, channel, enter_abuses, last_offense FROM Channels WHERE enter_abuses > 0');
$sth->execute();
return $sth->fetchall_arrayref({});
};
$self->{pbot}->{logger}->log($@) if $@;
return $channel_datas;
}
sub devalidate_channel {
2020-02-15 23:38:32 +01:00
my ($self, $id, $channel, $mode) = @_;
2020-02-15 23:38:32 +01:00
$mode = 0 if not defined $mode;
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare("UPDATE Channels SET validated = ? WHERE id = ? AND channel = ?");
$sth->execute($mode, $id, $channel);
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub devalidate_all_channels {
2020-02-15 23:38:32 +01:00
my ($self, $id, $mode) = @_;
2020-02-15 23:38:32 +01:00
$mode = 0 if not defined $mode;
2020-02-15 23:38:32 +01:00
my $where = '';
$where = 'WHERE id = ?' if defined $id;
2020-02-15 23:38:32 +01:00
eval {
my $sth = $self->{dbh}->prepare("UPDATE Channels SET validated = ? $where");
$sth->bind_param(1, $mode);
$sth->bind_param(2, $id) if defined $id;
$sth->execute();
$self->{new_entries}++;
};
$self->{pbot}->{logger}->log($@) if $@;
}
sub link_aliases {
2020-02-15 23:38:32 +01:00
my ($self, $account, $hostmask, $nickserv) = @_;
2020-02-15 23:38:32 +01:00
my $debug_link = $self->{pbot}->{registry}->get_value('messagehistory', 'debug_link');
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Linking [$account][" . ($hostmask ? $hostmask : 'undef') . "][" . ($nickserv ? $nickserv : 'undef') . "]\n") if $debug_link >= 3;
2020-02-15 23:38:32 +01:00
eval {
my %ids;
if ($hostmask) {
my ($nick, $host) = $hostmask =~ /^([^!]+)![^@]+@(.*)$/;
my $sth = $self->{dbh}->prepare('SELECT id, last_seen FROM Hostmasks WHERE host = ?');
$sth->execute($host);
my $rows = $sth->fetchall_arrayref({});
my $now = time;
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
my $idhost = $self->find_most_recent_hostmask($row->{id}) if $debug_link >= 2 && $row->{id} != $account;
if ($now - $row->{last_seen} <= 60 * 60 * 48) {
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}, force => 1};
$self->{pbot}->{logger}->log("found STRONG matching id $row->{id} ($idhost) for host [$host]\n") if $debug_link >= 2 && $row->{id} != $account;
} else {
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{WEAK}};
$self->{pbot}->{logger}->log("found WEAK matching id $row->{id} ($idhost) for host [$host]\n") if $debug_link >= 2 && $row->{id} != $account;
}
}
2020-02-15 23:38:32 +01:00
unless ($nick =~ m/^Guest\d+$/) {
my $sth = $self->{dbh}->prepare('SELECT id, hostmask FROM Hostmasks WHERE nick = ?');
$sth->execute($nick);
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
my ($account1) = $host =~ m{/([^/]+)$};
$account1 = '' if not defined $account1;
2020-02-15 23:38:32 +01:00
my $hostip = undef;
if ($host =~ m/(\d+[[:punct:]]\d+[[:punct:]]\d+[[:punct:]]\d+)\D/) {
$hostip = $1;
$hostip =~ s/[[:punct:]]/./g;
}
2016-08-26 13:31:51 +02:00
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
next if $row->{id} == $account;
$self->{pbot}->{logger}->log("Processing row $row->{hostmask}\n");
my ($thost) = $row->{hostmask} =~ m/@(.*)$/;
if ($thost =~ m{/}) {
my ($account2) = $thost =~ m{/([^/]+)$};
if ($account1 ne $account2) {
$self->{pbot}->{logger}->log("Skipping non-matching cloaked hosts: $host vs $thost\n");
next;
} else {
$self->{pbot}->{logger}->log("Cloaked hosts match: $host vs $thost\n");
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}, force => 1};
}
}
my $distance = fastdistance($host, $thost);
my $length = (length($host) > length($thost)) ? length $host : length $thost;
#$self->{pbot}->{logger}->log("distance: " . ($distance / $length) . " -- $host vs $thost\n") if $length != 0;
if ($length != 0 && $distance / $length < 0.50) {
$self->{pbot}->{logger}->log("11: distance match: $host vs $thost == " . ($distance / $length) . "\n");
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}}; # don't force linking
$self->{pbot}->{logger}->log("found STRONG matching id $row->{id} ($row->{hostmask}) for nick [$nick]\n") if $debug_link >= 2;
} else {
# handle cases like 99.57.140.149 vs 99-57-140-149.lightspeed.sntcca.sbcglobal.net
if (defined $hostip) {
if ($hostip eq $thost) {
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}}; # don't force linking
$self->{pbot}->{logger}->log("IP vs hostname match: $host vs $thost\n");
}
} elsif ($thost =~ m/(\d+[[:punct:]]\d+[[:punct:]]\d+[[:punct:]]\d+)\D/) {
my $thostip = $1;
$thostip =~ s/[[:punct:]]/./g;
if ($thostip eq $host) {
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}}; # don't force linking
$self->{pbot}->{logger}->log("IP vs hostname match: $host vs $thost\n");
}
}
}
}
}
2016-08-26 13:31:51 +02:00
}
2020-02-15 23:38:32 +01:00
if ($nickserv) {
my $sth = $self->{dbh}->prepare('SELECT id FROM Nickserv WHERE nickserv = ?');
$sth->execute($nickserv);
my $rows = $sth->fetchall_arrayref({});
2016-08-26 13:31:51 +02:00
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
my $idhost = $self->find_most_recent_hostmask($row->{id}) if $debug_link >= 2 && $row->{id} != $account;
$ids{$row->{id}} = {id => $row->{id}, type => $self->{alias_type}->{STRONG}, force => 1};
$self->{pbot}->{logger}->log("12: found STRONG matching id $row->{id} ($idhost) for nickserv [$nickserv]\n") if $debug_link >= 2 && $row->{id} != $account;
2016-08-26 13:31:51 +02:00
}
}
2020-02-15 23:38:32 +01:00
foreach my $id (sort keys %ids) {
next if $account == $id;
$self->link_alias($account, $id, $ids{$id}->{type}, $ids{$id}->{force});
}
};
$self->{pbot}->{logger}->log($@) if $@;
}
2015-05-12 21:59:45 +02:00
sub link_alias {
2020-02-15 23:38:32 +01:00
my ($self, $id, $alias, $type, $force) = @_;
my $debug_link = $self->{pbot}->{registry}->get_value('messagehistory', 'debug_link');
$self->{pbot}->{logger}
->log("Attempting to " . ($force ? "forcefully " : "") . ($type == $self->{alias_type}->{STRONG} ? "strongly" : "weakly") . " link $id to $alias\n")
if $debug_link >= 3;
my $ret = eval {
my $sth = $self->{dbh}->prepare('SELECT type FROM Aliases WHERE id = ? AND alias = ? LIMIT 1');
$sth->execute($alias, $id);
my $row = $sth->fetchrow_hashref();
if (defined $row) {
if ($force) {
if ($row->{'type'} != $type) {
$self->{pbot}->{logger}->log("$id already " . ($row->{'type'} == $self->{alias_type}->{STRONG} ? "strongly" : "weakly") . " linked to $alias, forcing override\n")
if $debug_link >= 1;
$sth = $self->{dbh}->prepare('UPDATE Aliases SET type = ? WHERE alias = ? AND id = ?');
$sth->execute($type, $id, $alias);
$sth->execute($type, $alias, $id);
return 1;
} else {
$self->{pbot}->{logger}->log("$id already " . ($row->{'type'} == $self->{alias_type}->{STRONG} ? "strongly" : "weakly") . " linked to $alias, ignoring\n")
if $debug_link >= 4;
return 0;
}
} else {
$self->{pbot}->{logger}->log("$id already " . ($row->{'type'} == $self->{alias_type}->{STRONG} ? "strongly" : "weakly") . " linked to $alias, ignoring\n")
if $debug_link >= 4;
return 0;
}
}
2016-08-25 10:41:32 +02:00
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('INSERT INTO Aliases VALUES (?, ?, ?)');
$sth->execute($alias, $id, $type);
$sth->execute($id, $alias, $type);
return 1;
};
$self->{pbot}->{logger}->log($@) if $@;
2016-08-25 10:41:32 +02:00
2020-02-15 23:38:32 +01:00
my $host1 = $self->find_most_recent_hostmask($id);
my $host2 = $self->find_most_recent_hostmask($alias);
$self->{pbot}->{logger}->log(($type == $self->{alias_type}->{STRONG} ? "Strongly" : "Weakly") . " linked $id ($host1) to $alias ($host2).\n") if $ret and $debug_link;
if ($ret) {
$host1 = lc $host1;
$host2 = lc $host2;
my ($nick1) = $host1 =~ m/^([^!]+)!/;
my ($nick2) = $host2 =~ m/^([^!]+)!/;
my $distance = fastdistance($nick1, $nick2);
my $length = (length $nick1 > length $nick2) ? length $nick1 : length $nick2;
my $irc_cloak = $self->{pbot}->{registry}->get_value('irc', 'cloak') // 'user';
if ($distance > 1 && ($nick1 !~ /^guest/ && $nick2 !~ /^guest/) && ($host1 !~ /$irc_cloak/ || $host2 !~ /$irc_cloak/)) {
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("[$nick1][$nick2] $distance / $length\n");
$self->{pbot}->{logger}->log("Possible bogus link: ($id) $host1 vs ($alias) $host2\n");
2016-08-25 10:41:32 +02:00
}
}
2020-02-15 23:38:32 +01:00
return $ret;
2015-05-12 21:59:45 +02:00
}
sub unlink_alias {
2020-02-15 23:38:32 +01:00
my ($self, $id, $alias) = @_;
my $ret = eval {
my $ret = 0;
my $sth = $self->{dbh}->prepare('DELETE FROM Aliases WHERE id = ? AND alias = ?');
$sth->execute($id, $alias);
if ($sth->rows) {
$self->{new_entries}++;
$ret = 1;
}
2015-05-12 21:59:45 +02:00
2020-02-15 23:38:32 +01:00
$sth->execute($alias, $id);
if ($sth->rows) {
$self->{new_entries}++;
$ret = 1;
} else {
$ret = 0;
}
return $ret;
};
$self->{pbot}->{logger}->log($@) if $@;
2015-05-12 21:59:45 +02:00
return $ret;
}
sub vacuum {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
eval { $self->{dbh}->commit(); };
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("SQLite error $@ when committing $self->{new_entries} entries.\n") if $@;
2020-02-15 23:38:32 +01:00
$self->{dbh}->do("VACUUM");
2020-02-15 23:38:32 +01:00
$self->{dbh}->begin_work();
$self->{new_entries} = 0;
}
sub rebuild_aliases_table {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
eval {
$self->{dbh}->do('DELETE FROM Aliases');
$self->vacuum;
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare('SELECT id, hostmask FROM Hostmasks ORDER BY id');
$sth->execute();
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT nickserv FROM Nickserv WHERE id = ?');
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
$self->{pbot}->{logger}->log("Link [$row->{id}][$row->{hostmask}]\n");
2020-02-15 23:38:32 +01:00
$self->link_aliases($row->{id}, $row->{hostmask});
2020-02-15 23:38:32 +01:00
$sth->execute($row->{id});
my $nrows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $nrow (@$nrows) { $self->link_aliases($row->{id}, undef, $nrow->{nickserv}); }
}
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
}
sub get_also_known_as {
2020-02-15 23:38:32 +01:00
my ($self, $nick, $dont_use_aliases_table) = @_;
my $debug = $self->{pbot}->{registry}->get_value('messagehistory', 'debug_aka');
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("Looking for AKAs for nick [$nick]\n") if $debug;
2020-02-15 23:38:32 +01:00
my %akas = eval {
my (%akas, %hostmasks, %ids);
2020-02-15 23:38:32 +01:00
unless ($dont_use_aliases_table) {
my ($id, $hostmask) = $self->find_message_account_by_nick($nick);
2020-02-15 23:38:32 +01:00
if (not defined $id) { return %akas; }
2020-02-15 23:38:32 +01:00
$ids{$id} = {id => $id, type => $self->{alias_type}->{STRONG}};
$self->{pbot}->{logger}->log("Adding $id -> $id\n") if $debug;
2020-02-15 23:38:32 +01:00
my $sth = $self->{dbh}->prepare('SELECT alias, type FROM Aliases WHERE id = ?');
$sth->execute($id);
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
2020-02-16 00:16:38 +01:00
# next if $row->{type} == $self->{alias_type}->{WEAK};
2020-02-15 23:38:32 +01:00
$ids{$row->{alias}} = {id => $id, type => $row->{type}};
$self->{pbot}->{logger}->log("[$id] 1) Adding $row->{alias} -> $id [type $row->{type}]\n") if $debug;
}
2020-02-15 23:38:32 +01:00
my %seen_id;
$sth = $self->{dbh}->prepare('SELECT id, type FROM Aliases WHERE alias = ?');
2016-02-10 16:10:37 +01:00
2020-02-15 23:38:32 +01:00
while (1) {
my $new_aliases = 0;
foreach my $id (keys %ids) {
next if $ids{$id}->{type} == $self->{alias_type}->{WEAK};
next if exists $seen_id{$id};
$seen_id{$id} = $id;
2020-02-15 23:38:32 +01:00
$sth->execute($id);
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
next if exists $ids{$row->{id}};
2020-02-15 23:38:32 +01:00
#next if $row->{type} == $self->{alias_type}->{WEAK};
$ids{$row->{id}} = {id => $id, type => $ids{$id}->{type} == $self->{alias_type}->{WEAK} ? $self->{alias_type}->{WEAK} : $row->{type}};
$new_aliases++;
$self->{pbot}->{logger}->log("[$id] 2) Adding $row->{id} -> $id [type $row->{type}]\n") if $debug;
}
}
last if not $new_aliases;
}
2016-02-10 12:42:42 +01:00
my $hostmask_sth = $self->{dbh}->prepare('SELECT hostmask, nickchange, last_seen FROM Hostmasks WHERE id = ?');
2020-02-15 23:38:32 +01:00
my $nickserv_sth = $self->{dbh}->prepare('SELECT nickserv FROM Nickserv WHERE id = ?');
my $gecos_sth = $self->{dbh}->prepare('SELECT gecos FROM Gecos WHERE id = ?');
2016-02-10 12:42:42 +01:00
2020-02-15 23:38:32 +01:00
my $csv = Text::CSV->new({binary => 1});
2020-02-15 23:38:32 +01:00
foreach my $id (keys %ids) {
$hostmask_sth->execute($id);
$rows = $hostmask_sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
my ($nick, $user, $host) = $row->{hostmask} =~ m/^([^!]+)!([^@]+)@(.*)/;
$akas{$row->{hostmask}} = {
id => $id,
alias => $ids{$id}->{id},
nick => $nick,
user => $user,
host => $host,
hostmask => $row->{hostmask},
type => $ids{$id}->{type},
nickchange => $row->{nickchange},
last_seen => $row->{last_seen},
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("[$id] Adding hostmask $row->{hostmask} -> $ids{$id}->{id} [type $ids{$id}->{type}]\n") if $debug;
}
2020-02-15 23:38:32 +01:00
$nickserv_sth->execute($id);
$rows = $nickserv_sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
foreach my $aka (keys %akas) {
if ($akas{$aka}->{id} == $id) {
if (exists $akas{$aka}->{nickserv}) { $akas{$aka}->{nickserv} .= ",$row->{nickserv}"; }
else { $akas{$aka}->{nickserv} = $row->{nickserv}; }
}
}
}
2020-02-15 23:38:32 +01:00
$gecos_sth->execute($id);
$rows = $gecos_sth->fetchall_arrayref({});
foreach my $row (@$rows) {
foreach my $aka (keys %akas) {
if ($akas{$aka}->{id} == $id) {
if (exists $akas{$aka}->{gecos}) {
$csv->parse($akas{$aka}->{gecos});
my @gecos = $csv->fields;
push @gecos, $row->{gecos};
$csv->combine(@gecos);
$akas{$aka}->{gecos} = $csv->string;
} else {
my @gecos = ($row->{gecos});
$csv->combine(@gecos);
$akas{$aka}->{gecos} = $csv->string;
}
}
}
}
}
2020-02-15 23:38:32 +01:00
return %akas;
}
my $sth = $self->{dbh}->prepare('SELECT id, hostmask FROM Hostmasks WHERE nick = ? ORDER BY last_seen DESC');
$sth->execute($nick);
my $rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
$hostmasks{$row->{hostmask}} = $row->{id};
$ids{$row->{id}} = $row->{hostmask};
$akas{$row->{hostmask}} = {hostmask => $row->{hostmask}, id => $row->{id}};
$self->{pbot}->{logger}->log("Found matching nick [$nick] for hostmask $row->{hostmask} with id $row->{id}\n");
}
2020-02-15 23:38:32 +01:00
foreach my $hostmask (keys %hostmasks) {
my ($host) = $hostmask =~ /(\@.*)$/;
$sth = $self->{dbh}->prepare('SELECT id FROM Hostmasks WHERE host = ?');
$sth->execute($host);
$rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) {
next if exists $ids{$row->{id}};
$ids{$row->{id}} = $row->{id};
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT hostmask FROM Hostmasks WHERE id == ?');
$sth->execute($row->{id});
my $rows = $sth->fetchall_arrayref({});
foreach my $nrow (@$rows) {
next if exists $akas{$nrow->{hostmask}};
$akas{$nrow->{hostmask}} = {hostmask => $nrow->{hostmask}, id => $row->{id}};
$self->{pbot}->{logger}->log("Adding matching host [$hostmask] and id [$row->{id}] AKA hostmask $nrow->{hostmask}\n");
}
}
}
2020-02-15 23:38:32 +01:00
my %nickservs;
foreach my $id (keys %ids) {
$sth = $self->{dbh}->prepare('SELECT nickserv FROM Nickserv WHERE id == ?');
$sth->execute($id);
$rows = $sth->fetchall_arrayref({});
2020-02-15 23:38:32 +01:00
foreach my $row (@$rows) { $nickservs{$row->{nickserv}} = $id; }
}
2020-02-15 23:38:32 +01:00
foreach my $nickserv (sort keys %nickservs) {
foreach my $aka (keys %akas) {
if ($akas{$aka}->{id} == $nickservs{$nickserv}) {
if (exists $akas{$aka}->{nickserv}) { $akas{$aka}->{nickserv} .= ",$nickserv"; }
else { $akas{$aka}->{nickserv} = $nickserv; }
}
}
2020-02-15 23:38:32 +01:00
$sth = $self->{dbh}->prepare('SELECT id FROM Nickserv WHERE nickserv == ?');
$sth->execute($nickserv);
$rows = $sth->fetchall_arrayref({});
foreach my $row (@$rows) {
next if exists $ids{$row->{id}};
$ids{$row->{id}} = $row->{id};
$sth = $self->{dbh}->prepare('SELECT hostmask FROM Hostmasks WHERE id == ?');
$sth->execute($row->{id});
my $rows = $sth->fetchall_arrayref({});
foreach my $nrow (@$rows) {
if (exists $akas{$nrow->{hostmask}}) {
if (exists $akas{$nrow->{hostmask}}->{nickserv}) { $akas{$nrow->{hostmask}}->{nickserv} .= ",$nickserv"; }
else { $akas{$nrow->{hostmask}}->{nickserv} = $nickserv; }
} else {
$akas{$nrow->{hostmask}} = {hostmask => $nrow->{hostmask}, id => $row->{id}, nickserv => $nickserv};
$self->{pbot}->{logger}->log("Adding matching nickserv [$nickserv] and id [$row->{id}] AKA hostmask $nrow->{hostmask}\n");
}
}
}
}
2020-02-15 23:38:32 +01:00
foreach my $id (keys %ids) {
$sth = $self->{dbh}->prepare('SELECT hostmask FROM Hostmasks WHERE id == ?');
$sth->execute($id);
$rows = $sth->fetchall_arrayref({});
foreach my $row (@$rows) {
next if exists $akas{$row->{hostmask}};
$akas{$row->{hostmask}} = {hostmask => $row->{hostmask}, id => $id};
$self->{pbot}->{logger}->log("Adding matching id [$id] AKA hostmask $row->{hostmask}\n");
}
}
2020-02-15 23:38:32 +01:00
return %akas;
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("bad aka: $@") if $@;
return %akas;
}
sub get_ancestor_id {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
2020-02-15 23:38:32 +01:00
$id = 0 if not defined $id;
2020-02-15 23:38:32 +01:00
my $ancestor = eval {
my $sth = $self->{dbh}->prepare('SELECT id FROM Aliases WHERE alias = ? ORDER BY id LIMIT 1');
$sth->execute($id);
my $row = $sth->fetchrow_hashref();
return defined $row ? $row->{id} : 0;
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
2020-02-15 23:38:32 +01:00
return $id if not $ancestor;
return $ancestor < $id ? $ancestor : $id;
}
# End of public API, the remaining are internal support routines for this module
sub get_new_account_id {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
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};
};
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log($@) if $@;
return ++$id;
}
sub get_message_account_id {
2020-02-15 23:38:32 +01:00
my ($self, $mask) = @_;
my $id = eval {
my $sth = $self->{dbh}->prepare('SELECT id FROM Hostmasks WHERE hostmask == ?');
$sth->execute($mask);
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 {
2020-02-15 23:38:32 +01:00
my $self = shift;
2020-02-15 23:38:32 +01:00
return if not $self->{dbh};
2020-09-13 01:28:44 +02:00
return if $self->{pbot}->{child}; # don't commit() as child of fork()
2017-08-28 22:53:03 +02:00
2020-02-15 23:38:32 +01:00
if ($self->{new_entries} > 0) {
# $self->{pbot}->{logger}->log("Commiting $self->{new_entries} messages to SQLite\n");
eval { $self->{dbh}->commit(); };
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("SQLite error $@ when committing $self->{new_entries} entries.\n") if $@;
$self->{dbh}->begin_work();
$self->{new_entries} = 0;
}
}
1;