2021-07-21 08:20:24 +02:00
# File: Core.pm
2010-03-17 07:36:54 +01:00
#
2021-07-21 08:20:24 +02:00
# Purpose: PBot IRC Bot Core
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;
2021-07-10 21:43:42 +02:00
# 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.
2010-03-17 07:36:54 +01:00
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
2017-03-05 22:33:31 +01:00
2021-07-21 08:20:24 +02:00
package PBot::Core ;
2010-03-17 07:36:54 +01:00
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
2010-03-17 07:36:54 +01:00
use Carp ( ) ;
2021-07-21 07:44:51 +02:00
use PBot::Core::Logger ;
use PBot::Core::AntiFlood ;
use PBot::Core::AntiSpam ;
2021-11-20 03:05:50 +01:00
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 ;
2010-03-17 07:36:54 +01:00
2021-07-09 00:47:29 +02:00
use Encode ;
use File::Basename ;
2021-06-07 04:12:14 +02:00
# 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 ;
2010-03-17 07:36:54 +01:00
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 ;
2010-03-22 08:33:44 +01:00
}
sub initialize {
2020-02-15 23:38:32 +01:00
my ( $ self , % conf ) = @ _ ;
2021-06-05 22:20:03 +02:00
$ self - > { startup_timestamp } = time ;
2020-02-15 23:38:32 +01:00
2021-06-05 22:20:03 +02:00
# process command-line arguments for path and registry overrides
2020-02-15 23:38:32 +01:00
foreach my $ arg ( @ ARGV ) {
2021-11-20 03:05:50 +01:00
if ( $ arg =~ m/^-?(?:general\.)?((?:data|applet|update)_dir)=(.*)$/ ) {
2020-05-27 04:21:11 +02:00
# check command-line arguments for directory overrides
2020-04-21 02:53:32 +02:00
my $ override = $ 1 ;
my $ value = $ 2 ;
2020-05-30 03:57:22 +02:00
$ value =~ s/[\\\/]$// ; # strip trailing directory separator
2021-06-05 22:20:03 +02:00
$ conf { data_dir } = $ value if $ override eq 'data_dir' ;
2021-11-20 03:05:50 +01:00
$ conf { applet_dir } = $ value if $ override eq 'applet_dir' ;
2021-06-05 22:20:03 +02:00
$ conf { update_dir } = $ value if $ override eq 'update_dir' ;
2020-05-27 04:21:11 +02:00
} else {
# check command-line arguments for registry overrides
my ( $ item , $ value ) = split /=/ , $ arg , 2 ;
if ( not defined $ item or not defined $ value ) {
2021-06-21 05:31:47 +02:00
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" ;
2020-05-27 04:21:11 +02:00
exit ;
}
my ( $ section , $ key ) = split /\./ , $ item , 2 ;
2021-06-05 22:20:03 +02:00
2020-05-27 04:21:11 +02:00
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
}
2019-12-22 04:04:39 +01:00
}
2021-06-05 22:20:03 +02:00
# make sure the paths exist
2021-11-20 03:05:50 +01:00
foreach my $ path ( qw/data_dir applet_dir update_dir/ ) {
2021-06-05 22:20:03 +02:00
if ( not - d $ conf { $ path } ) {
print STDERR "$path path ($conf{$path}) does not exist; aborting.\n" ;
exit ;
}
2020-02-14 22:32:12 +01:00
}
2021-07-09 00:47:29 +02:00
# 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 ;
}
2021-06-05 22:20:03 +02:00
# let modules register atexit subroutines
2021-07-21 07:44:51 +02:00
$ self - > { atexit } = PBot::Core::Registerable - > new ( pbot = > $ self , % conf ) ;
2021-06-05 22:20:03 +02:00
# register default signal handlers
2020-09-29 21:29:40 +02:00
$ self - > register_signal_handlers ;
2021-06-05 22:20:03 +02:00
# 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
2021-07-28 07:38:19 +02:00
$ self - > { version } = PBot::VERSION - > new ( pbot = > $ self ) ;
$ self - > { logger } - > log ( $ self - > { version } - > version . "\n" ) ;
2021-07-27 21:25:56 +02:00
2021-06-05 22:20:03 +02:00
# log command-line arguments
$ self - > { logger } - > log ( "Args: @ARGV\n" ) if @ ARGV ;
2020-02-14 22:32:12 +01:00
2021-06-05 22:20:03 +02:00
# log configured paths
2021-11-20 03:05:50 +01:00
$ self - > { logger } - > log ( "applet_dir: $conf{applet_dir}\n" ) ;
2021-06-05 22:20:03 +02:00
$ 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
2021-06-05 22:20:03 +02: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
2021-06-05 22:20:03 +02:00
# --- 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
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
2021-06-05 22:20:03 +02: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' ) ) {
2021-06-05 22:20:03 +02:00
$ 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 ;
}
2021-06-21 05:31:47 +02:00
# prepare the IRC engine
2021-07-21 07:44:51 +02:00
$ self - > { irc } = PBot::Core::IRC - > new ( pbot = > $ self ) ;
2021-06-21 05:31:47 +02:00
2021-06-05 22:20:03 +02:00
# 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 ) ;
2021-11-20 03:05:50 +01:00
$ 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 ;
2021-07-21 06:38:07 +02:00
2021-06-05 22:20:03 +02:00
# register command/factoid interpreters
2020-02-15 23:38:32 +01:00
$ self - > { interpreter } - > register ( sub { $ self - > { commands } - > interpreter ( @ _ ) } ) ;
2021-07-27 06:39:44 +02:00
$ self - > { interpreter } - > register ( sub { $ self - > { factoids } - > { interpreter } - > interpreter ( @ _ ) } ) ;
2020-02-15 23:38:32 +01:00
# give botowner all capabilities
2021-06-05 22:20:03 +02:00
# -- this must happen last after all modules have registered their capabilities --
$ self - > { capabilities } - > rebuild_botowner_capabilities ;
2020-05-15 01:57:34 +02:00
2021-06-22 02:26:24 +02:00
# fire all pending save events at exit
2020-05-15 01:57:34 +02:00
$ self - > { atexit } - > register ( sub {
2021-06-22 02:26:24 +02:00
$ self - > { event_queue } - > execute_and_dequeue_event ( 'save .*' ) ;
2020-05-15 01:57:34 +02:00
}
) ;
2021-07-27 21:01:42 +02:00
$ self - > { logger } - > log ( "PBot::Core initialized.\n" ) ;
2010-03-17 07:36:54 +01:00
}
2015-01-11 00:56:43 +01:00
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 ;
2015-01-11 00:56:43 +01:00
}
2010-03-17 07:36:54 +01:00
2021-06-05 22:20:03 +02:00
# TODO: add disconnect subroutine and connect/disconnect/reconnect commands
2010-03-17 07:36:54 +01:00
sub connect {
2021-06-05 22:20:03 +02:00
my ( $ self ) = @ _ ;
2020-07-12 02:38:24 +02:00
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
2021-06-05 22:20:03 +02: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 = (
2021-07-04 08:12:34 +02:00
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 ,
2021-06-05 22:20:03 +02:00
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' ) ,
2021-06-12 10:23:37 +02:00
Debug = > $ self - > { registry } - > get_value ( 'irc' , 'debug' ) ,
2021-07-04 08:12:34 +02:00
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
2021-06-05 22:20:03 +02: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 ;
2021-07-21 06:38:07 +02:00
# set up IRC handlers
$ self - > { irchandlers } - > add_handlers ;
2010-03-17 07:36:54 +01:00
}
2014-05-17 00:11:31 +02:00
sub register_signal_handlers {
2021-06-24 01:42:15 +02:00
my ( $ self ) = @ _ ;
2021-06-05 22:20:03 +02:00
$ SIG { INT } = sub {
2021-06-12 10:23:37 +02:00
my $ msg = "SIGINT received, exiting immediately.\n" ;
2021-06-12 11:18:59 +02:00
if ( exists $ self - > { logger } ) {
2021-06-12 10:23:37 +02:00
$ self - > { logger } - > log ( $ msg ) ;
} else {
print $ msg ;
}
2021-06-07 06:44:42 +02:00
$ self - > atexit ;
exit 0 ;
2021-06-05 22:20:03 +02:00
} ;
2014-05-17 00:11:31 +02:00
}
2021-06-05 22:20:03 +02:00
# called when PBot terminates
2014-05-17 00:11:31 +02:00
sub atexit {
2021-06-24 01:42:15 +02:00
my ( $ self ) = @ _ ;
2020-02-15 23:38:32 +01:00
$ self - > { atexit } - > execute_all ;
2021-06-12 10:23:37 +02:00
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 ;
2014-05-17 00:11:31 +02:00
}
2021-06-05 22:20:03 +02:00
# main loop
sub do_one_loop {
2021-06-21 05:31:47 +02:00
my ( $ self ) = @ _ ;
2021-06-22 02:26:24 +02:00
# do an irc engine loop (select, eventqueues, etc)
$ self - > { irc } - > do_one_loop ;
2021-06-22 02:40:36 +02:00
# invoke PBot events (returns seconds until next event)
my $ waitfor = $ self - > { event_queue } - > do_events ;
2021-06-22 02:26:24 +02:00
# tell irc select loop to sleep for this many seconds
# (or until its own internal eventqueue has an event)
$ self - > { irc } - > timeout ( $ waitfor ) ;
2020-01-25 21:28:05 +01:00
}
2021-06-05 22:20:03 +02:00
# main entry point
sub start {
2021-06-24 01:42:15 +02:00
my ( $ self ) = @ _ ;
2021-06-21 05:31:47 +02:00
$ self - > connect ;
2021-06-05 22:20:03 +02:00
while ( 1 ) {
$ self - > do_one_loop ;
2020-02-15 23:38:32 +01:00
}
2020-01-25 21:28:05 +01:00
}
2010-03-17 07:36:54 +01:00
1 ;