2010-03-17 07:36:54 +01:00
# File: PBot.pm
#
2021-06-05 22:20:03 +02:00
# Purpose: IRC Bot
2021-06-12 11:26:16 +02:00
#
# PBot was started around 2001, 2002. 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-10 21:43:42 +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.
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
2010-03-17 07:36:54 +01:00
package PBot::PBot ;
2021-06-19 06:23:34 +02:00
use PBot::Imports ;
2019-07-11 03:40:53 +02:00
2010-03-17 07:36:54 +01:00
use Carp ( ) ;
use PBot::Logger ;
2018-01-23 08:48:25 +01:00
use PBot::VERSION ;
2010-03-17 07:36:54 +01:00
use PBot::AntiFlood ;
2018-08-06 07:41:08 +02:00
use PBot::AntiSpam ;
2021-06-05 22:20:03 +02:00
use PBot::BanList ;
use PBot::BlackList ;
use PBot::Capabilities ;
2010-03-22 08:33:44 +01:00
use PBot::Commands ;
2021-06-05 22:20:03 +02:00
use PBot::Channels ;
2010-03-22 08:33:44 +01:00
use PBot::ChanOps ;
2021-06-05 22:20:03 +02:00
use PBot::DualIndexHashObject ;
use PBot::DualIndexSQLiteObject ;
use PBot::EventDispatcher ;
2021-06-22 02:26:24 +02:00
use PBot::EventQueue ;
2019-06-26 18:34:19 +02:00
use PBot::Factoids ;
2020-02-14 07:36:05 +01:00
use PBot::Functions ;
2021-06-05 22:20:03 +02:00
use PBot::HashObject ;
use PBot::IgnoreList ;
use PBot::Interpreter ;
use PBot::IRC ;
use PBot::IRCHandlers ;
use PBot::LagChecker ;
use PBot::MessageHistory ;
2020-02-15 03:52:41 +01:00
use PBot::Modules ;
2021-06-05 22:20:03 +02:00
use PBot::MiscCommands ;
use PBot::NickList ;
use PBot::Plugins ;
2020-02-15 03:41:00 +01:00
use PBot::ProcessManager ;
2021-06-05 22:20:03 +02:00
use PBot::Registry ;
use PBot::Refresher ;
use PBot::SelectHandler ;
use PBot::StdinReader ;
2020-04-21 02:53:32 +02:00
use PBot::Updater ;
2021-06-05 22:20:03 +02:00
use PBot::Users ;
use PBot::Utils::ParseDate ;
use PBot::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 ) {
2020-04-21 02:53:32 +02:00
if ( $ arg =~ m/^-?(?:general\.)?((?:data|module|plugin|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' ;
$ 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' ;
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
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 ;
}
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
$ self - > { atexit } = PBot::Registerable - > new ( pbot = > $ self , % conf ) ;
# 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
$ self - > { logger } = PBot::Logger - > new ( pbot = > $ self , filename = > "$conf{data_dir}/log/log" , % conf ) ;
2020-04-20 19:53:35 +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
$ 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
2021-06-05 22:20:03 +02: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
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-06-05 22:20:03 +02:00
$ 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
2021-06-05 22:20:03 +02:00
$ self - > { commands } = PBot::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
# add 'cap' capability command here since $self->{commands} is created after $self->{capabilities}
2020-05-04 22:21:35 +02:00
$ self - > { commands } - > register ( sub { $ self - > { capabilities } - > cmd_cap ( @ _ ) } , "cap" ) ;
2020-02-15 23:38:32 +01:00
2021-06-05 22:20:03 +02: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" ) ;
2021-06-05 22:20:03 +02:00
# 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' ) ) {
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-05 18:38:37 +02:00
$ self - > { irc } = PBot::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-06-22 02:26:24 +02:00
$ 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 ) ;
2021-06-05 22:20:03 +02:00
$ 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 ) ;
2020-04-29 06:33:49 +02:00
$ self - > { banlist } = PBot::BanList - > new ( pbot = > $ self , % conf ) ;
2021-06-05 22:20:03 +02:00
$ 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 ) ;
2021-06-05 22:20:03 +02:00
$ 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
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 ( @ _ ) } ) ;
$ self - > { interpreter } - > register ( sub { $ self - > { factoids } - > interpreter ( @ _ ) } ) ;
# 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
return ;
}
) ;
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 ;
# 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 (
[
2021-06-25 03:28:49 +02:00
'myinfo' ,
2020-02-15 23:38:32 +01:00
'whoisserver' ,
'whoiscountry' ,
'whoischannels' ,
'whoisidle' ,
'motdstart' ,
'endofmotd' ,
'away' ,
] ,
sub { }
) ;
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 ;