3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-19 18:39:35 +01:00
pbot/PBot/PBot.pm

341 lines
12 KiB
Perl
Raw Normal View History

# File: PBot.pm
# Author: pragma_
#
# Purpose: IRC Bot
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
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::PBot;
use strict; use warnings;
2019-07-11 03:40:53 +02:00
use feature 'unicode_strings';
use utf8;
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;
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;
use PBot::Timer;
2020-04-21 02:53:32 +02:00
use PBot::Updater;
use PBot::Users;
use PBot::Utils::ParseDate;
use PBot::WebPaste;
# unbuffer stdout stream
STDOUT->autoflush(1);
# set standard output streams to encode as utf8
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
# decode command-line arguments from utf8
use Encode;
@ARGV = map { decode('UTF-8', $_, 1) } @ARGV;
sub new {
2020-02-15 23:38:32 +01:00
my ($proto, %conf) = @_;
my $class = ref($proto) || $proto;
my $self = bless {}, $class;
$self->initialize(%conf);
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{plugin_dir} = $value if $override eq 'plugin_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` (e.g.: irc.botnick=newnick)\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
foreach my $path (qw/data_dir module_dir plugin_dir update_dir/) {
if (not -d $conf{$path}) {
print STDERR "$path path ($conf{$path}) does not exist; aborting.\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("plugin_dir: $conf{plugin_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 remaining core PBot modules -- do not change this order
$self->{timer} = PBot::Timer->new(pbot => $self, timeout => 10, name => 'PBot Timer', %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->{irc} = PBot::IRC->new();
$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;
# flush all pending save events to disk at exit
$self->{atexit}->register(sub {
2020-05-30 05:48:53 +02:00
$self->{timer}->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
}
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;
2020-02-15 23:38:32 +01:00
$self->{logger}->log("Connecting to $server:$port\n");
2020-02-15 23:38:32 +01:00
while (
not $self->{conn} = $self->{irc}->newconn(
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'),
Server => $server,
Port => $port,
2020-02-15 23:38:32 +01:00
Pacing => 1,
UTF8 => 1,
SSL => $self->{registry}->get_value('irc', 'SSL'),
SSL_ca_file => $self->{registry}->get_value('irc', 'SSL_ca_file'),
SSL_ca_path => $self->{registry}->get_value('irc', 'SSL_ca_path'),
Debug => $self->{registry}->get_value('irc', 'debug'),
2020-02-15 23:38:32 +01:00
)
)
{
$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;
# start timer once connected
$self->{timer}->start;
# 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(
[
'whoisserver',
'whoiscountry',
'whoischannels',
'whoisidle',
'motdstart',
'endofmotd',
'away',
],
sub { }
);
}
sub register_signal_handlers {
2020-02-15 23:38:32 +01:00
my $self = shift;
$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 {
2020-02-15 23:38:32 +01:00
my $self = shift;
$self->{atexit}->execute_all;
alarm 0;
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 = shift;
$self->{irc}->do_one_loop() if $self->{connected};
$self->{select_handler}->do_select;
}
# main entry point
sub start {
my $self = shift;
while (1) {
$self->connect if not $self->{connected};
$self->do_one_loop;
2020-02-15 23:38:32 +01:00
}
}
1;