pbot/lib/PBot/Core.pm

359 lines
13 KiB
Perl
Raw Permalink Normal View History

2021-07-21 08:20:24 +02:00
# File: Core.pm
#
2021-07-21 08:20:24 +02:00
# Purpose: PBot IRC Bot Core
2021-06-12 11:26:16 +02:00
#
2023-02-21 06:31:52 +01:00
# PBot was started around 1997-2001. 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
#
2021-07-21 07:44:51 +02:00
# PBot has forked the Net::IRC package internally as PBot::Core::IRC. It
# contains numerous bugfixes and supports various new features such as IRCv3
# client capability negotiation and SASL user authentication.
2023-02-21 06:31:52 +01:00
# SPDX-FileCopyrightText: 2001-2023 Pragmatic Software <pragma78@gmail.com>
2021-07-11 00:00:22 +02:00
# 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
2021-07-21 08:20:24 +02:00
package PBot::Core;
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2021-07-21 07:44:51 +02:00
use PBot::VERSION;
2019-07-11 03:40:53 +02:00
use Carp ();
2021-07-21 07:44:51 +02:00
use PBot::Core::Logger;
use PBot::Core::AntiFlood;
use PBot::Core::AntiSpam;
use PBot::Core::Applets;
2021-07-21 07:44:51 +02:00
use PBot::Core::BanList;
use PBot::Core::BlackList;
use PBot::Core::Capabilities;
use PBot::Core::Commands;
use PBot::Core::Channels;
use PBot::Core::ChanOps;
use PBot::Core::EventDispatcher;
use PBot::Core::EventQueue;
use PBot::Core::Factoids;
use PBot::Core::Functions;
2021-07-24 01:57:33 +02:00
use PBot::Core::Handlers;
2021-07-21 07:44:51 +02:00
use PBot::Core::IgnoreList;
use PBot::Core::Interpreter;
use PBot::Core::IRC;
use PBot::Core::IRCHandlers;
use PBot::Core::LagChecker;
use PBot::Core::MessageHistory;
use PBot::Core::NickList;
use PBot::Core::Plugins;
use PBot::Core::ProcessManager;
use PBot::Core::Registry;
use PBot::Core::Refresher;
use PBot::Core::SelectHandler;
use PBot::Core::StdinReader;
2021-07-24 04:22:25 +02:00
use PBot::Core::Storage::HashObject;
use PBot::Core::Storage::DualIndexHashObject;
use PBot::Core::Storage::DualIndexSQLiteObject;
2021-07-21 07:44:51 +02:00
use PBot::Core::Updater;
use PBot::Core::Users;
2021-07-24 04:22:25 +02:00
use PBot::Core::Utils::ParseDate;
2021-07-21 07:44:51 +02:00
use PBot::Core::WebPaste;
use POSIX qw/EXIT_FAILURE EXIT_SUCCESS/;
use Encode;
use File::Basename;
BEGIN {
# 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($class, %args) {
2021-06-19 06:23:34 +02:00
my $self = bless {}, $class;
$self->initialize(%args);
2020-02-15 23:38:32 +01:00
return $self;
}
sub initialize($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) {
if ($arg =~ m/^-?(?:general\.)?((?:data|applet|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{applet_dir} = $value if $override eq 'applet_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 EXIT_FAILURE;
}
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 EXIT_FAILURE;
}
$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
foreach my $path (qw/data_dir applet_dir update_dir/) {
if (not -d $conf{$path}) {
print STDERR "$path path ($conf{$path}) does not exist; aborting.\n";
exit EXIT_FAILURE;
}
}
# 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 EXIT_FAILURE;
}
# let modules register atexit subroutines
2021-07-21 07:44:51 +02:00
$self->{atexit} = PBot::Core::Registerable->new(pbot => $self, %conf);
# register default signal handlers
$self->register_signal_handlers;
# prepare and open logger
2021-07-21 07:44:51 +02:00
$self->{logger} = PBot::Core::Logger->new(pbot => $self, filename => "$conf{data_dir}/log/log", %conf);
2020-04-20 19:53:35 +02:00
2021-07-27 21:25:56 +02:00
# log the version
$self->{version} = PBot::VERSION->new(pbot => $self);
$self->{logger}->log($self->{version}->version . "\n");
2021-07-27 21:25:56 +02:00
# log command-line arguments
$self->{logger}->log("Args: @ARGV\n") if @ARGV;
# log configured paths
$self->{logger}->log("applet_dir: $conf{applet_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
2021-07-21 07:44:51 +02:00
$self->{updater} = PBot::Core::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! ---
if ($self->{updater}->update != EXIT_SUCCESS) {
2020-04-21 02:53:32 +02:00
$self->{logger}->log("Update failed.\n");
exit EXIT_FAILURE;
2020-04-20 19:53:35 +02:00
}
# create capabilities so commands can add new capabilities
2021-07-21 07:44:51 +02:00
$self->{capabilities} = PBot::Core::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
2021-07-21 07:44:51 +02:00
$self->{commands} = PBot::Core::Commands->new(pbot => $self, filename => "$conf{data_dir}/commands", %conf);
2020-02-15 23:38:32 +01:00
# prepare registry
2021-07-21 07:44:51 +02:00
$self->{registry} = PBot::Core::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. See doc/QuickStart.md for more information.\n");
exit EXIT_FAILURE;
2020-02-15 23:38:32 +01:00
}
# prepare the IRC engine
2021-07-21 07:44:51 +02:00
$self->{irc} = PBot::Core::IRC->new(pbot => $self);
# prepare remaining core PBot modules -- do not change this order
2021-07-21 07:44:51 +02:00
$self->{event_queue} = PBot::Core::EventQueue->new(pbot => $self, name => 'PBot event queue', %conf);
$self->{event_dispatcher} = PBot::Core::EventDispatcher->new(pbot => $self, %conf);
$self->{users} = PBot::Core::Users->new(pbot => $self, filename => "$conf{data_dir}/users", %conf);
$self->{antiflood} = PBot::Core::AntiFlood->new(pbot => $self, %conf);
$self->{antispam} = PBot::Core::AntiSpam->new(pbot => $self, %conf);
$self->{applets} = PBot::Core::Applets->new(pbot => $self, %conf);
2021-07-21 07:44:51 +02:00
$self->{banlist} = PBot::Core::BanList->new(pbot => $self, %conf);
$self->{blacklist} = PBot::Core::BlackList->new(pbot => $self, filename => "$conf{data_dir}/blacklist", %conf);
$self->{channels} = PBot::Core::Channels->new(pbot => $self, filename => "$conf{data_dir}/channels", %conf);
$self->{chanops} = PBot::Core::ChanOps->new(pbot => $self, %conf);
$self->{factoids} = PBot::Core::Factoids->new(pbot => $self, filename => "$conf{data_dir}/factoids.sqlite3", %conf);
$self->{functions} = PBot::Core::Functions->new(pbot => $self, %conf);
$self->{refresher} = PBot::Core::Refresher->new(pbot => $self);
2021-07-24 01:57:33 +02:00
$self->{handlers} = PBot::Core::Handlers->new(pbot => $self, %conf);
2021-07-21 07:44:51 +02:00
$self->{ignorelist} = PBot::Core::IgnoreList->new(pbot => $self, filename => "$conf{data_dir}/ignorelist", %conf);
$self->{irchandlers} = PBot::Core::IRCHandlers->new(pbot => $self, %conf);
$self->{interpreter} = PBot::Core::Interpreter->new(pbot => $self, %conf);
$self->{lagchecker} = PBot::Core::LagChecker->new(pbot => $self, %conf);
$self->{messagehistory} = PBot::Core::MessageHistory->new(pbot => $self, filename => "$conf{data_dir}/message_history.sqlite3", %conf);
$self->{nicklist} = PBot::Core::NickList->new(pbot => $self, %conf);
2021-07-24 04:22:25 +02:00
$self->{parsedate} = PBot::Core::Utils::ParseDate->new(pbot => $self, %conf);
2021-07-21 07:44:51 +02:00
$self->{plugins} = PBot::Core::Plugins->new(pbot => $self, %conf);
$self->{process_manager} = PBot::Core::ProcessManager->new(pbot => $self, %conf);
$self->{select_handler} = PBot::Core::SelectHandler->new(pbot => $self, %conf);
$self->{stdin_reader} = PBot::Core::StdinReader->new(pbot => $self, %conf);
$self->{webpaste} = PBot::Core::WebPaste->new(pbot => $self, %conf);
2020-02-15 23:38:32 +01:00
2021-07-24 04:22:25 +02:00
# load commands in Commands directory
$self->{commands}->load_commands;
# 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}->interpreter(@_) });
2020-02-15 23:38:32 +01:00
# 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 .*');
}
);
$self->{logger}->log("PBot::Core initialized.\n");
}
sub random_nick($self, $length = undef) {
2020-02-15 23:38:32 +01:00
$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($self) {
return if $ENV{PBOT_LOCAL};
2020-02-15 23:38:32 +01:00
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
if ($self->{conn}) {
$self->{logger}->log("Error: already connected to $self->{server}.\n");
return;
}
$self->{server} = "$server:$port";
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
}
if (!$self->{conn}) {
$self->{logger}->log("Max retries reached; giving up.\n");
$self->exit(EXIT_FAILURE);
}
2020-02-15 23:38:32 +01:00
# set up IRC handlers
$self->{irchandlers}->add_handlers;
}
sub register_signal_handlers($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 EXIT_SUCCESS;
};
}
# called when PBot terminates
sub atexit($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($self, $exitval) {
$exitval //= EXIT_SUCCESS;
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($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($self) {
$self->connect;
while (1) {
$self->do_one_loop;
2020-02-15 23:38:32 +01:00
}
}
1;