2021-07-20 23:20:24 -07:00
# File: Core.pm
2010-03-17 06:36:54 +00:00
#
2021-07-20 23:20:24 -07:00
# Purpose: PBot IRC Bot Core
2021-06-12 02:26:16 -07:00
#
2021-07-13 19:45:56 -07:00
# PBot was started around 2004, 2005. It has been lovingly maintained;
2021-07-10 12:43:42 -07: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 02:26:16 -07:00
#
2021-07-20 22:44:51 -07: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 06:36:54 +00:00
2021-07-10 15:00:22 -07:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
2017-03-05 21:33:31 +00:00
2021-07-20 23:20:24 -07:00
package PBot::Core ;
2010-03-17 06:36:54 +00:00
2021-06-18 21:23:34 -07:00
use PBot::Imports ;
2021-07-20 22:44:51 -07:00
use PBot::VERSION ;
2019-07-10 18:40:53 -07:00
2010-03-17 06:36:54 +00:00
use Carp ( ) ;
2021-07-20 22:44:51 -07:00
use PBot::Core::Logger ;
use PBot::Core::AntiFlood ;
use PBot::Core::AntiSpam ;
2021-11-19 18:05:50 -08:00
use PBot::Core::Applets ;
2021-07-20 22:44:51 -07: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-23 16:57:33 -07:00
use PBot::Core::Handlers ;
2021-07-20 22:44:51 -07: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-23 19:22:25 -07:00
use PBot::Core::Storage::HashObject ;
use PBot::Core::Storage::DualIndexHashObject ;
use PBot::Core::Storage::DualIndexSQLiteObject ;
2021-07-20 22:44:51 -07:00
use PBot::Core::Updater ;
use PBot::Core::Users ;
2021-07-23 19:22:25 -07:00
use PBot::Core::Utils::ParseDate ;
2021-07-20 22:44:51 -07:00
use PBot::Core::WebPaste ;
2010-03-17 06:36:54 +00:00
2021-07-08 15:47:29 -07:00
use Encode ;
use File::Basename ;
2021-06-06 19:12:14 -07: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 06:36:54 +00:00
sub new {
2021-06-18 21:23:34 -07:00
my ( $ class , % args ) = @ _ ;
my $ self = bless { } , $ class ;
$ self - > initialize ( % args ) ;
2020-02-15 14:38:32 -08:00
return $ self ;
2010-03-22 07:33:44 +00:00
}
sub initialize {
2020-02-15 14:38:32 -08:00
my ( $ self , % conf ) = @ _ ;
2021-06-05 13:20:03 -07:00
$ self - > { startup_timestamp } = time ;
2020-02-15 14:38:32 -08:00
2021-06-05 13:20:03 -07:00
# process command-line arguments for path and registry overrides
2020-02-15 14:38:32 -08:00
foreach my $ arg ( @ ARGV ) {
2021-11-19 18:05:50 -08:00
if ( $ arg =~ m/^-?(?:general\.)?((?:data|applet|update)_dir)=(.*)$/ ) {
2020-05-26 19:21:11 -07:00
# check command-line arguments for directory overrides
2020-04-20 17:53:32 -07:00
my $ override = $ 1 ;
my $ value = $ 2 ;
2020-05-29 18:57:22 -07:00
$ value =~ s/[\\\/]$// ; # strip trailing directory separator
2021-06-05 13:20:03 -07:00
$ conf { data_dir } = $ value if $ override eq 'data_dir' ;
2021-11-19 18:05:50 -08:00
$ conf { applet_dir } = $ value if $ override eq 'applet_dir' ;
2021-06-05 13:20:03 -07:00
$ conf { update_dir } = $ value if $ override eq 'update_dir' ;
2020-05-26 19:21:11 -07: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-20 20:31:47 -07: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-26 19:21:11 -07:00
exit ;
}
my ( $ section , $ key ) = split /\./ , $ item , 2 ;
2021-06-05 13:20:03 -07:00
2020-05-26 19:21:11 -07: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 14:38:32 -08:00
}
2019-12-21 19:04:39 -08:00
}
2021-06-05 13:20:03 -07:00
# make sure the paths exist
2021-11-19 18:05:50 -08:00
foreach my $ path ( qw/data_dir applet_dir update_dir/ ) {
2021-06-05 13:20:03 -07:00
if ( not - d $ conf { $ path } ) {
print STDERR "$path path ($conf{$path}) does not exist; aborting.\n" ;
exit ;
}
2020-02-14 13:32:12 -08:00
}
2021-07-08 15:47:29 -07: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 13:20:03 -07:00
# let modules register atexit subroutines
2021-07-20 22:44:51 -07:00
$ self - > { atexit } = PBot::Core::Registerable - > new ( pbot = > $ self , % conf ) ;
2021-06-05 13:20:03 -07:00
# register default signal handlers
2020-09-29 12:29:40 -07:00
$ self - > register_signal_handlers ;
2021-06-05 13:20:03 -07:00
# prepare and open logger
2021-07-20 22:44:51 -07:00
$ self - > { logger } = PBot::Core::Logger - > new ( pbot = > $ self , filename = > "$conf{data_dir}/log/log" , % conf ) ;
2020-04-20 10:53:35 -07:00
2021-07-27 12:25:56 -07:00
# log the version
2021-07-27 22:38:19 -07:00
$ self - > { version } = PBot::VERSION - > new ( pbot = > $ self ) ;
$ self - > { logger } - > log ( $ self - > { version } - > version . "\n" ) ;
2021-07-27 12:25:56 -07:00
2021-06-05 13:20:03 -07:00
# log command-line arguments
$ self - > { logger } - > log ( "Args: @ARGV\n" ) if @ ARGV ;
2020-02-14 13:32:12 -08:00
2021-06-05 13:20:03 -07:00
# log configured paths
2021-11-19 18:05:50 -08:00
$ self - > { logger } - > log ( "applet_dir: $conf{applet_dir}\n" ) ;
2021-06-05 13:20:03 -07:00
$ self - > { logger } - > log ( " data_dir: $conf{data_dir}\n" ) ;
$ self - > { logger } - > log ( "update_dir: $conf{update_dir}\n" ) ;
2020-02-15 14:38:32 -08:00
2021-06-05 13:20:03 -07:00
# prepare the updater
2021-07-20 22:44:51 -07:00
$ self - > { updater } = PBot::Core::Updater - > new ( pbot = > $ self , data_dir = > $ conf { data_dir } , update_dir = > $ conf { update_dir } ) ;
2020-04-20 10:53:35 -07:00
2020-04-20 17:53:32 -07:00
# update any data files to new locations/formats
2021-06-05 13:20:03 -07:00
# --- this must happen before any data files are opened! ---
2020-04-20 17:53:32 -07:00
if ( $ self - > { updater } - > update ) {
$ self - > { logger } - > log ( "Update failed.\n" ) ;
2020-04-20 10:53:35 -07:00
exit 0 ;
}
# create capabilities so commands can add new capabilities
2021-07-20 22:44:51 -07:00
$ self - > { capabilities } = PBot::Core::Capabilities - > new ( pbot = > $ self , filename = > "$conf{data_dir}/capabilities" , % conf ) ;
2020-02-15 14:38:32 -08:00
2020-04-20 10:53:35 -07:00
# create commands so the modules can register new commands
2021-07-20 22:44:51 -07:00
$ self - > { commands } = PBot::Core::Commands - > new ( pbot = > $ self , filename = > "$conf{data_dir}/commands" , % conf ) ;
2020-02-15 14:38:32 -08:00
2021-06-05 13:20:03 -07:00
# prepare registry
2021-07-20 22:44:51 -07:00
$ self - > { registry } = PBot::Core::Registry - > new ( pbot = > $ self , filename = > "$conf{data_dir}/registry" , % conf ) ;
2020-02-15 14:38:32 -08:00
# ensure user has attempted to configure the bot
if ( not length $ self - > { registry } - > get_value ( 'irc' , 'botnick' ) ) {
2021-06-05 13:20:03 -07: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 14:38:32 -08:00
exit ;
}
2021-06-20 20:31:47 -07:00
# prepare the IRC engine
2021-07-20 22:44:51 -07:00
$ self - > { irc } = PBot::Core::IRC - > new ( pbot = > $ self ) ;
2021-06-20 20:31:47 -07:00
2021-06-05 13:20:03 -07:00
# prepare remaining core PBot modules -- do not change this order
2021-07-20 22:44:51 -07: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-19 18:05:50 -08:00
$ self - > { applets } = PBot::Core::Applets - > new ( pbot = > $ self , % conf ) ;
2021-07-20 22:44:51 -07: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-23 16:57:33 -07:00
$ self - > { handlers } = PBot::Core::Handlers - > new ( pbot = > $ self , % conf ) ;
2021-07-20 22:44:51 -07: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-23 19:22:25 -07:00
$ self - > { parsedate } = PBot::Core::Utils::ParseDate - > new ( pbot = > $ self , % conf ) ;
2021-07-20 22:44:51 -07: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 14:38:32 -08:00
2021-07-23 19:22:25 -07:00
# load commands in Commands directory
$ self - > { commands } - > load_commands ;
2021-07-20 21:38:07 -07:00
2021-06-05 13:20:03 -07:00
# register command/factoid interpreters
2020-02-15 14:38:32 -08:00
$ self - > { interpreter } - > register ( sub { $ self - > { commands } - > interpreter ( @ _ ) } ) ;
2021-07-26 21:39:44 -07:00
$ self - > { interpreter } - > register ( sub { $ self - > { factoids } - > { interpreter } - > interpreter ( @ _ ) } ) ;
2020-02-15 14:38:32 -08:00
# give botowner all capabilities
2021-06-05 13:20:03 -07:00
# -- this must happen last after all modules have registered their capabilities --
$ self - > { capabilities } - > rebuild_botowner_capabilities ;
2020-05-14 16:57:34 -07:00
2021-06-21 17:26:24 -07:00
# fire all pending save events at exit
2020-05-14 16:57:34 -07:00
$ self - > { atexit } - > register ( sub {
2021-06-21 17:26:24 -07:00
$ self - > { event_queue } - > execute_and_dequeue_event ( 'save .*' ) ;
2020-05-14 16:57:34 -07:00
}
) ;
2021-07-27 12:01:42 -07:00
$ self - > { logger } - > log ( "PBot::Core initialized.\n" ) ;
2010-03-17 06:36:54 +00:00
}
2015-01-10 15:56:43 -08:00
sub random_nick {
2020-02-15 14:38:32 -08: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-10 15:56:43 -08:00
}
2010-03-17 06:36:54 +00:00
2021-06-05 13:20:03 -07:00
# TODO: add disconnect subroutine and connect/disconnect/reconnect commands
2010-03-17 06:36:54 +00:00
sub connect {
2021-06-05 13:20:03 -07:00
my ( $ self ) = @ _ ;
2020-07-11 17:38:24 -07:00
return if $ ENV { PBOT_LOCAL } ;
2020-02-15 14:38:32 -08:00
if ( $ self - > { connected } ) {
# TODO: disconnect, clean-up, etc
}
2021-07-09 23:12:29 -07: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 14:38:32 -08:00
2021-06-05 13:20:03 -07:00
$ self - > { logger } - > log ( "Connecting to $server:$port\n" ) ;
2020-02-15 14:38:32 -08:00
2021-07-09 23:12:29 -07:00
for ( my $ attempt = 0 ; $ attempt < $ retries ; $ attempt + + ) {
my % config = (
2021-07-03 23:12:34 -07: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 14:38:32 -08:00
Server = > $ server ,
2021-06-05 13:20:03 -07:00
Port = > $ port ,
2020-02-15 14:38:32 -08:00
Pacing = > 1 ,
UTF8 = > 1 ,
2021-07-10 22:47:33 -07:00
TLS = > $ self - > { registry } - > get_value ( 'irc' , 'tls' ) ,
2021-06-12 01:23:37 -07:00
Debug = > $ self - > { registry } - > get_value ( 'irc' , 'debug' ) ,
2021-07-03 23:12:34 -07:00
PBot = > $ self ,
2021-07-09 23:12:29 -07:00
) ;
2021-07-10 22:47:33 -07:00
# set TLS stuff
my $ tls_ca_file = $ self - > { registry } - > get_value ( 'irc' , 'tls_ca_file' ) ;
2021-07-09 23:12:29 -07:00
2021-07-10 22:47:33 -07:00
if ( length $ tls_ca_file and $ tls_ca_file ne 'none' ) {
$ config { TLS_ca_file } = $ tls_ca_file ;
2021-07-09 23:12:29 -07:00
}
2021-07-10 22:47:33 -07:00
my $ tls_ca_path = $ self - > { registry } - > get_value ( 'irc' , 'tls_ca_path' ) ;
2021-07-09 23:12:29 -07:00
2021-07-10 22:47:33 -07:00
if ( length $ tls_ca_file and $ tls_ca_file ne 'none' ) {
$ config { TLS_ca_file } = $ tls_ca_file ;
2021-07-09 23:12:29 -07:00
}
# attempt to connect
$ self - > { conn } = $ self - > { irc } - > newconn ( % config ) ;
# connection succeeded
last if $ self - > { conn } ;
# connection failed
2021-06-05 13:20:03 -07:00
$ self - > { logger } - > log ( "$0: Can't connect to $server:$port: $!\nRetrying in $delay seconds...\n" ) ;
sleep $ delay ;
2020-02-15 14:38:32 -08:00
}
$ self - > { connected } = 1 ;
2021-07-20 21:38:07 -07:00
# set up IRC handlers
$ self - > { irchandlers } - > add_handlers ;
2010-03-17 06:36:54 +00:00
}
2014-05-16 22:11:31 +00:00
sub register_signal_handlers {
2021-06-23 16:42:15 -07:00
my ( $ self ) = @ _ ;
2021-06-05 13:20:03 -07:00
$ SIG { INT } = sub {
2021-06-12 01:23:37 -07:00
my $ msg = "SIGINT received, exiting immediately.\n" ;
2021-06-12 02:18:59 -07:00
if ( exists $ self - > { logger } ) {
2021-06-12 01:23:37 -07:00
$ self - > { logger } - > log ( $ msg ) ;
} else {
print $ msg ;
}
2021-06-06 21:44:42 -07:00
$ self - > atexit ;
exit 0 ;
2021-06-05 13:20:03 -07:00
} ;
2014-05-16 22:11:31 +00:00
}
2021-06-05 13:20:03 -07:00
# called when PBot terminates
2014-05-16 22:11:31 +00:00
sub atexit {
2021-06-23 16:42:15 -07:00
my ( $ self ) = @ _ ;
2020-02-15 14:38:32 -08:00
$ self - > { atexit } - > execute_all ;
2021-06-12 01:23:37 -07: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-16 22:11:31 +00:00
}
2021-06-05 13:20:03 -07:00
# main loop
sub do_one_loop {
2021-06-20 20:31:47 -07:00
my ( $ self ) = @ _ ;
2021-06-21 17:26:24 -07:00
# do an irc engine loop (select, eventqueues, etc)
$ self - > { irc } - > do_one_loop ;
2021-06-21 17:40:36 -07:00
# invoke PBot events (returns seconds until next event)
my $ waitfor = $ self - > { event_queue } - > do_events ;
2021-06-21 17:26:24 -07: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 12:28:05 -08:00
}
2021-06-05 13:20:03 -07:00
# main entry point
sub start {
2021-06-23 16:42:15 -07:00
my ( $ self ) = @ _ ;
2021-06-20 20:31:47 -07:00
$ self - > connect ;
2021-06-05 13:20:03 -07:00
while ( 1 ) {
$ self - > do_one_loop ;
2020-02-15 14:38:32 -08:00
}
2020-01-25 12:28:05 -08:00
}
2010-03-17 06:36:54 +00:00
1 ;