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

378 lines
13 KiB
Perl
Raw Normal View History

# File: PBot.pm
#
# Purpose: IRC Bot
2021-06-12 11:26:16 +02:00
#
2021-07-14 04:45:56 +02:00
# PBot was started around 2004, 2005. It has been lovingly maintained;
# however, it does use the ancient but simple Net::IRC package (if it
# ain't broke) instead of packages based on significantly more complex
# Enterprise-level event-loop frameworks. PBot uses pure Perl 5 blessed
# classes instead of something like Moo or Object::Pad, though this may
# change eventually.
2021-06-12 11:26:16 +02:00
#
# PBot has forked the Net::IRC package internally as PBot::IRC. It contains
# numerous bugfixes and supports various new features such as IRCv3 client
# capability negotiation and SASL user authentication.
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::PBot;
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2019-07-11 03:40:53 +02:00
use Carp ();
use PBot::Logger;
use PBot::VERSION;
use PBot::AntiFlood;
2018-08-06 07:41:08 +02:00
use PBot::AntiSpam;
use PBot::BanList;
use PBot::BlackList;
use PBot::Capabilities;
use PBot::Commands;
use PBot::Channels;
use PBot::ChanOps;
use PBot::DualIndexHashObject;
use PBot::DualIndexSQLiteObject;
use PBot::EventDispatcher;
use PBot::EventQueue;
2019-06-26 18:34:19 +02:00
use PBot::Factoids;
use PBot::Functions;
use PBot::HashObject;
use PBot::IgnoreList;
use PBot::Interpreter;
use PBot::IRC;
use PBot::IRCHandlers;
use PBot::LagChecker;
use PBot::MessageHistory;
use PBot::Modules;
use PBot::MiscCommands;
use PBot::NickList;
use PBot::Plugins;
use PBot::ProcessManager;
use PBot::Registry;
use PBot::Refresher;
use PBot::SelectHandler;
use PBot::StdinReader;
2020-04-21 02:53:32 +02:00
use PBot::Updater;
use PBot::Users;
use PBot::Utils::ParseDate;
use PBot::WebPaste;
use Encode;
use File::Basename;
# set standard output streams to encode as utf8
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
# decode command-line arguments from utf8
@ARGV = map { decode('UTF-8', $_, 1) } @ARGV;
sub new {
2021-06-19 06:23:34 +02:00
my ($class, %args) = @_;
my $self = bless {}, $class;
$self->initialize(%args);
2020-02-15 23:38:32 +01:00
return $self;
}
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
$self->{startup_timestamp} = time;
2020-02-15 23:38:32 +01:00
# process command-line arguments for path and registry overrides
2020-02-15 23:38:32 +01:00
foreach my $arg (@ARGV) {
2020-04-21 02:53:32 +02:00
if ($arg =~ m/^-?(?:general\.)?((?:data|module|plugin|update)_dir)=(.*)$/) {
# check command-line arguments for directory overrides
2020-04-21 02:53:32 +02:00
my $override = $1;
my $value = $2;
$value =~ s/[\\\/]$//; # strip trailing directory separator
$conf{data_dir} = $value if $override eq 'data_dir';
$conf{module_dir} = $value if $override eq 'module_dir';
$conf{update_dir} = $value if $override eq 'update_dir';
} else {
# check command-line arguments for registry overrides
my ($item, $value) = split /=/, $arg, 2;
if (not defined $item or not defined $value) {
print STDERR "Fatal error: unknown argument `$arg`; arguments must be in the form of `section.key=value` or `path_dir=value` (e.g.: irc.botnick=newnick or data_dir=path)\n";
exit;
}
my ($section, $key) = split /\./, $item, 2;
if (not defined $section or not defined $key) {
print STDERR "Fatal error: bad argument `$arg`; registry entries must be in the form of section.key (e.g.: irc.botnick)\n";
exit;
}
$section =~ s/^-//; # remove a leading - to allow arguments like -irc.botnick due to habitual use of -args
$self->{overrides}->{"$section.$key"} = $value;
2020-02-15 23:38:32 +01:00
}
}
# make sure the paths exist
2021-07-14 04:45:56 +02:00
foreach my $path (qw/data_dir module_dir update_dir/) {
if (not -d $conf{$path}) {
print STDERR "$path path ($conf{$path}) does not exist; aborting.\n";
exit;
}
}
# insist that data directory be copied
if (basename($conf{data_dir}) eq 'data') {
print STDERR "Data directory ($conf{data_dir}) cannot be named `data`. This is to ensure the directory is copied from its default location. Please follow doc/QuickStart.md.\n";
exit;
}
# let modules register atexit subroutines
$self->{atexit} = PBot::Registerable->new(pbot => $self, %conf);
# register default signal handlers
$self->register_signal_handlers;
# prepare and open logger
$self->{logger} = PBot::Logger->new(pbot => $self, filename => "$conf{data_dir}/log/log", %conf);
2020-04-20 19:53:35 +02:00
# log command-line arguments
$self->{logger}->log("Args: @ARGV\n") if @ARGV;
# log configured paths
$self->{logger}->log("module_dir: $conf{module_dir}\n");
$self->{logger}->log(" data_dir: $conf{data_dir}\n");
$self->{logger}->log("update_dir: $conf{update_dir}\n");
2020-02-15 23:38:32 +01:00
# prepare the updater
$self->{updater} = PBot::Updater->new(pbot => $self, data_dir => $conf{data_dir}, update_dir => $conf{update_dir});
2020-04-20 19:53:35 +02:00
2020-04-21 02:53:32 +02:00
# update any data files to new locations/formats
# --- this must happen before any data files are opened! ---
2020-04-21 02:53:32 +02:00
if ($self->{updater}->update) {
$self->{logger}->log("Update failed.\n");
2020-04-20 19:53:35 +02:00
exit 0;
}
# create capabilities so commands can add new capabilities
$self->{capabilities} = PBot::Capabilities->new(pbot => $self, filename => "$conf{data_dir}/capabilities", %conf);
2020-02-15 23:38:32 +01:00
2020-04-20 19:53:35 +02:00
# create commands so the modules can register new commands
$self->{commands} = PBot::Commands->new(pbot => $self, filename => "$conf{data_dir}/commands", %conf);
2020-02-15 23:38:32 +01:00
# add 'cap' capability command here since $self->{commands} is created after $self->{capabilities}
$self->{commands}->register(sub { $self->{capabilities}->cmd_cap(@_) }, "cap");
2020-02-15 23:38:32 +01:00
# prepare the version information and `version` command
2020-02-15 23:38:32 +01:00
$self->{version} = PBot::VERSION->new(pbot => $self, %conf);
$self->{logger}->log($self->{version}->version . "\n");
# prepare registry
$self->{registry} = PBot::Registry->new(pbot => $self, filename => "$conf{data_dir}/registry", %conf);
2020-02-15 23:38:32 +01:00
# ensure user has attempted to configure the bot
if (not length $self->{registry}->get_value('irc', 'botnick')) {
$self->{logger}->log("Fatal error: IRC nickname not defined; please set registry key irc.botnick in $conf{data_dir}/registry to continue.\n");
2020-02-15 23:38:32 +01:00
exit;
}
# prepare the IRC engine
$self->{irc} = PBot::IRC->new(pbot => $self);
# prepare remaining core PBot modules -- do not change this order
$self->{event_queue} = PBot::EventQueue->new(pbot => $self, name => 'PBot event queue', %conf);
2020-02-15 23:38:32 +01:00
$self->{event_dispatcher} = PBot::EventDispatcher->new(pbot => $self, %conf);
$self->{users} = PBot::Users->new(pbot => $self, filename => "$conf{data_dir}/users", %conf);
2020-02-15 23:38:32 +01:00
$self->{antiflood} = PBot::AntiFlood->new(pbot => $self, %conf);
$self->{antispam} = PBot::AntiSpam->new(pbot => $self, %conf);
$self->{banlist} = PBot::BanList->new(pbot => $self, %conf);
$self->{blacklist} = PBot::BlackList->new(pbot => $self, filename => "$conf{data_dir}/blacklist", %conf);
$self->{channels} = PBot::Channels->new(pbot => $self, filename => "$conf{data_dir}/channels", %conf);
$self->{chanops} = PBot::ChanOps->new(pbot => $self, %conf);
$self->{factoids} = PBot::Factoids->new(pbot => $self, filename => "$conf{data_dir}/factoids.sqlite3", %conf);
$self->{functions} = PBot::Functions->new(pbot => $self, %conf);
$self->{refresher} = PBot::Refresher->new(pbot => $self);
$self->{ignorelist} = PBot::IgnoreList->new(pbot => $self, filename => "$conf{data_dir}/ignorelist", %conf);
$self->{irchandlers} = PBot::IRCHandlers->new(pbot => $self, %conf);
$self->{interpreter} = PBot::Interpreter->new(pbot => $self, %conf);
$self->{lagchecker} = PBot::LagChecker->new(pbot => $self, %conf);
$self->{misc_commands} = PBot::MiscCommands->new(pbot => $self, %conf);
$self->{messagehistory} = PBot::MessageHistory->new(pbot => $self, filename => "$conf{data_dir}/message_history.sqlite3", %conf);
$self->{modules} = PBot::Modules->new(pbot => $self, %conf);
2020-02-15 23:38:32 +01:00
$self->{nicklist} = PBot::NickList->new(pbot => $self, %conf);
$self->{parsedate} = PBot::Utils::ParseDate->new(pbot => $self, %conf);
$self->{plugins} = PBot::Plugins->new(pbot => $self, %conf);
$self->{process_manager} = PBot::ProcessManager->new(pbot => $self, %conf);
$self->{select_handler} = PBot::SelectHandler->new(pbot => $self, %conf);
$self->{stdin_reader} = PBot::StdinReader->new(pbot => $self, %conf);
$self->{webpaste} = PBot::WebPaste->new(pbot => $self, %conf);
2020-02-15 23:38:32 +01:00
# register command/factoid interpreters
2020-02-15 23:38:32 +01:00
$self->{interpreter}->register(sub { $self->{commands}->interpreter(@_) });
$self->{interpreter}->register(sub { $self->{factoids}->interpreter(@_) });
# give botowner all capabilities
# -- this must happen last after all modules have registered their capabilities --
$self->{capabilities}->rebuild_botowner_capabilities;
# fire all pending save events at exit
$self->{atexit}->register(sub {
$self->{event_queue}->execute_and_dequeue_event('save .*');
return;
}
);
}
sub random_nick {
2020-02-15 23:38:32 +01:00
my ($self, $length) = @_;
$length //= 9;
my @chars = ("A" .. "Z", "a" .. "z", "0" .. "9");
my $nick = $chars[rand @chars - 10]; # nicks cannot start with a digit
$nick .= $chars[rand @chars] for 1 .. $length;
return $nick;
}
# TODO: add disconnect subroutine and connect/disconnect/reconnect commands
sub connect {
my ($self) = @_;
return if $ENV{PBOT_LOCAL};
2020-02-15 23:38:32 +01:00
if ($self->{connected}) {
# TODO: disconnect, clean-up, etc
}
2021-07-10 08:12:29 +02:00
my $server = $self->{registry}->get_value('irc', 'server');
my $port = $self->{registry}->get_value('irc', 'port');
my $delay = $self->{registry}->get_value('irc', 'reconnect_delay') // 10;
my $retries = $self->{registry}->get_value('irc', 'reconnect_retries') // 10;
2020-02-15 23:38:32 +01:00
$self->{logger}->log("Connecting to $server:$port\n");
2020-02-15 23:38:32 +01:00
2021-07-10 08:12:29 +02:00
for (my $attempt = 0; $attempt < $retries; $attempt++) {
my %config = (
Nick => $self->{registry}->get_value('irc', 'randomize_nick') ? $self->random_nick : $self->{registry}->get_value('irc', 'botnick'),
Username => $self->{registry}->get_value('irc', 'username'),
Ircname => $self->{registry}->get_value('irc', 'realname'),
2020-02-15 23:38:32 +01:00
Server => $server,
Port => $port,
2020-02-15 23:38:32 +01:00
Pacing => 1,
UTF8 => 1,
2021-07-11 07:47:33 +02:00
TLS => $self->{registry}->get_value('irc', 'tls'),
Debug => $self->{registry}->get_value('irc', 'debug'),
PBot => $self,
2021-07-10 08:12:29 +02:00
);
2021-07-11 07:47:33 +02:00
# set TLS stuff
my $tls_ca_file = $self->{registry}->get_value('irc', 'tls_ca_file');
2021-07-10 08:12:29 +02:00
2021-07-11 07:47:33 +02:00
if (length $tls_ca_file and $tls_ca_file ne 'none') {
$config{TLS_ca_file} = $tls_ca_file;
2021-07-10 08:12:29 +02:00
}
2021-07-11 07:47:33 +02:00
my $tls_ca_path = $self->{registry}->get_value('irc', 'tls_ca_path');
2021-07-10 08:12:29 +02:00
2021-07-11 07:47:33 +02:00
if (length $tls_ca_file and $tls_ca_file ne 'none') {
$config{TLS_ca_file} = $tls_ca_file;
2021-07-10 08:12:29 +02:00
}
# attempt to connect
$self->{conn} = $self->{irc}->newconn(%config);
# connection succeeded
last if $self->{conn};
# connection failed
$self->{logger}->log("$0: Can't connect to $server:$port: $!\nRetrying in $delay seconds...\n");
sleep $delay;
2020-02-15 23:38:32 +01:00
}
$self->{connected} = 1;
# set up handlers for the IRC engine
$self->{conn}->add_default_handler(sub { $self->{irchandlers}->default_handler(@_) }, 1);
$self->{conn}->add_handler([251, 252, 253, 254, 255, 302], sub { $self->{irchandlers}->on_init(@_) });
# ignore these events
$self->{conn}->add_handler(
[
'myinfo',
2020-02-15 23:38:32 +01:00
'whoisserver',
'whoiscountry',
'whoischannels',
'whoisidle',
'motdstart',
'endofmotd',
'away',
],
sub { }
);
}
sub register_signal_handlers {
my ($self) = @_;
$SIG{INT} = sub {
my $msg = "SIGINT received, exiting immediately.\n";
2021-06-12 11:18:59 +02:00
if (exists $self->{logger}) {
$self->{logger}->log($msg);
} else {
print $msg;
}
$self->atexit;
exit 0;
};
}
# called when PBot terminates
sub atexit {
my ($self) = @_;
2020-02-15 23:38:32 +01:00
$self->{atexit}->execute_all;
if (exists $self->{logger}) {
$self->{logger}->log("Good-bye.\n");
} else {
print "Good-bye.\n";
}
}
# convenient function to exit PBot
sub exit {
my ($self, $exitval) = @_;
$exitval //= 0;
my $msg = "Exiting immediately.\n";
if (exists $self->{logger}) {
$self->{logger}->log($msg);
} else {
print $msg;
}
$self->atexit;
exit $exitval;
}
# main loop
sub do_one_loop {
my ($self) = @_;
# do an irc engine loop (select, eventqueues, etc)
$self->{irc}->do_one_loop;
# invoke PBot events (returns seconds until next event)
my $waitfor = $self->{event_queue}->do_events;
# tell irc select loop to sleep for this many seconds
# (or until its own internal eventqueue has an event)
$self->{irc}->timeout($waitfor);
}
# main entry point
sub start {
my ($self) = @_;
$self->connect;
while (1) {
$self->do_one_loop;
2020-02-15 23:38:32 +01:00
}
}
1;