2020-02-15 03:52:41 +01:00
# File: Modules.pm
2021-06-19 06:23:34 +02:00
#
2021-06-07 04:12:14 +02:00
# Purpose: Modules are command-line programs and scripts that can be loaded
# via PBot factoids. Command arguments are passed as command-line arguments.
# The standard output from the script is returned as the bot command result.
# The standard error output is stored in a file named <module>-stderr in the
# modules/ directory.
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
2020-02-15 03:52:41 +01:00
2021-07-21 07:44:51 +02:00
package PBot::Core::Modules ;
use parent 'PBot::Core::Class' ;
2020-02-15 03:52:41 +01:00
2021-06-19 06:23:34 +02:00
use PBot::Imports ;
2020-02-15 03:52:41 +01:00
use IPC::Run qw/run timeout/ ;
use Encode ;
sub initialize {
2020-02-15 23:38:32 +01:00
my ( $ self , % conf ) = @ _ ;
2021-06-07 04:12:14 +02:00
# bot commands to load and unload modules
2020-05-04 22:21:35 +02:00
$ self - > { pbot } - > { commands } - > register ( sub { $ self - > cmd_load ( @ _ ) } , "load" , 1 ) ;
$ self - > { pbot } - > { commands } - > register ( sub { $ self - > cmd_unload ( @ _ ) } , "unload" , 1 ) ;
2020-02-15 03:52:41 +01:00
}
2020-05-04 22:21:35 +02:00
sub cmd_load {
my ( $ self , $ context ) = @ _ ;
2021-06-07 04:12:14 +02:00
2020-05-02 05:59:51 +02:00
my ( $ keyword , $ module ) = $ self - > { pbot } - > { interpreter } - > split_args ( $ context - > { arglist } , 2 ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
return "Usage: load <keyword> <module>" if not defined $ module ;
2021-07-09 23:39:35 +02:00
my $ factoids = $ self - > { pbot } - > { factoids } - > { storage } ;
2021-06-07 04:12:14 +02:00
if ( $ factoids - > exists ( '.*' , $ keyword ) ) {
return 'There is already a keyword named ' . $ factoids - > get_data ( '.*' , $ keyword , '_name' ) . '.' ;
}
2020-02-15 23:38:32 +01:00
2020-05-04 22:21:35 +02:00
$ self - > { pbot } - > { factoids } - > add_factoid ( 'module' , '.*' , $ context - > { hostmask } , $ keyword , $ module , 1 ) ;
2021-06-07 04:12:14 +02:00
$ factoids - > set ( '.*' , $ keyword , 'add_nick' , 1 , 1 ) ;
2020-02-15 23:38:32 +01:00
$ factoids - > set ( '.*' , $ keyword , 'nooverride' , 1 ) ;
2021-06-07 04:12:14 +02:00
2020-05-04 22:21:35 +02:00
$ self - > { pbot } - > { logger } - > log ( "$context->{hostmask} loaded module $keyword => $module\n" ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
return "Loaded module $keyword => $module" ;
2020-02-15 03:52:41 +01:00
}
2020-05-04 22:21:35 +02:00
sub cmd_unload {
my ( $ self , $ context ) = @ _ ;
2021-06-07 04:12:14 +02:00
2020-05-02 05:59:51 +02:00
my $ module = $ self - > { pbot } - > { interpreter } - > shift_arg ( $ context - > { arglist } ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
return "Usage: unload <keyword>" if not defined $ module ;
2021-06-07 04:12:14 +02:00
2021-07-09 23:39:35 +02:00
my $ factoids = $ self - > { pbot } - > { factoids } - > { storage } ;
2020-02-15 23:38:32 +01:00
2021-06-07 04:12:14 +02:00
if ( not $ factoids - > exists ( '.*' , $ module ) ) {
return "/say $module not found." ;
}
if ( $ factoids - > get_data ( '.*' , $ module , 'type' ) ne 'module' ) {
return "/say " . $ factoids - > get_data ( '.*' , $ module , '_name' ) . ' is not a module.' ;
}
2020-02-15 23:38:32 +01:00
my $ name = $ factoids - > get_data ( '.*' , $ module , '_name' ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
$ factoids - > remove ( '.*' , $ module ) ;
2021-06-07 04:12:14 +02:00
2020-05-04 22:21:35 +02:00
$ self - > { pbot } - > { logger } - > log ( "$context->{hostmask} unloaded module $module\n" ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
return "/say $name unloaded." ;
2020-02-15 03:52:41 +01:00
}
sub execute_module {
2020-05-02 05:59:51 +02:00
my ( $ self , $ context ) = @ _ ;
2020-02-15 23:38:32 +01:00
my $ text ;
if ( $ self - > { pbot } - > { registry } - > get_value ( 'general' , 'debugcontext' ) ) {
use Data::Dumper ;
$ Data:: Dumper:: Sortkeys = 1 ;
$ self - > { pbot } - > { logger } - > log ( "execute_module\n" ) ;
2020-05-02 05:59:51 +02:00
$ self - > { pbot } - > { logger } - > log ( Dumper $ context ) ;
2020-02-15 23:38:32 +01:00
}
2020-02-15 03:52:41 +01:00
2020-05-02 05:59:51 +02:00
$ self - > { pbot } - > { process_manager } - > execute_process ( $ context , sub { $ self - > launch_module ( @ _ ) } ) ;
2020-02-15 03:52:41 +01:00
}
sub launch_module {
2020-05-02 05:59:51 +02:00
my ( $ self , $ context ) = @ _ ;
2021-06-07 04:12:14 +02:00
$ context - > { arguments } // = '' ;
2020-05-02 05:59:51 +02:00
my @ factoids = $ self - > { pbot } - > { factoids } - > find_factoid ( $ context - > { from } , $ context - > { keyword } , exact_channel = > 2 , exact_trigger = > 2 ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
if ( not @ factoids or not $ factoids [ 0 ] ) {
2020-05-02 05:59:51 +02:00
$ context - > { checkflood } = 1 ;
$ self - > { pbot } - > { interpreter } - > handle_result ( $ context , "/msg $context->{nick} Failed to find module for '$context->{keyword}' in channel $context->{from}\n" ) ;
2020-02-15 23:38:32 +01:00
return ;
2020-02-15 03:52:41 +01:00
}
2020-02-15 23:38:32 +01:00
my ( $ channel , $ trigger ) = ( $ factoids [ 0 ] - > [ 0 ] , $ factoids [ 0 ] - > [ 1 ] ) ;
2021-06-07 04:12:14 +02:00
2020-05-02 05:59:51 +02:00
$ context - > { channel } = $ channel ;
$ context - > { keyword } = $ trigger ;
$ context - > { trigger } = $ trigger ;
2020-02-15 23:38:32 +01:00
2021-07-09 23:39:35 +02:00
my $ module = $ self - > { pbot } - > { factoids } - > { storage } - > get_data ( $ channel , $ trigger , 'action' ) ;
2021-06-07 04:12:14 +02:00
2020-05-22 04:23:30 +02:00
$ self - > { pbot } - > { logger } - > log (
2021-06-07 04:12:14 +02:00
'(' . ( defined $ context - > { from } ? $ context - > { from } : "(undef)" ) . '): '
. "$context->{hostmask}: Executing module [$context->{command}] $module $context->{arguments}\n"
2020-05-22 04:23:30 +02:00
) ;
2020-06-21 05:55:22 +02:00
$ context - > { arguments } = $ self - > { pbot } - > { factoids } - > expand_factoid_vars ( $ context , $ context - > { arguments } ) ;
2020-02-15 23:38:32 +01:00
my $ module_dir = $ self - > { pbot } - > { registry } - > get_value ( 'general' , 'module_dir' ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
if ( not chdir $ module_dir ) {
$ self - > { pbot } - > { logger } - > log ( "Could not chdir to '$module_dir': $!\n" ) ;
Carp:: croak ( "Could not chdir to '$module_dir': $!" ) ;
2020-02-15 03:52:41 +01:00
}
2020-02-15 23:38:32 +01:00
2021-07-09 23:39:35 +02:00
if ( $ self - > { pbot } - > { factoids } - > { storage } - > exists ( $ channel , $ trigger , 'workdir' ) ) {
chdir $ self - > { pbot } - > { factoids } - > { storage } - > get_data ( $ channel , $ trigger , 'workdir' ) ;
2020-02-15 23:38:32 +01:00
}
# FIXME -- add check to ensure $module exists
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
my ( $ exitval , $ stdout , $ stderr ) = eval {
2020-05-02 05:59:51 +02:00
my $ args = $ context - > { arguments } ;
2021-06-07 04:12:14 +02:00
if ( not $ context - > { args_utf8 } ) {
$ args = encode ( 'UTF-8' , $ args ) ;
}
2020-02-15 23:38:32 +01:00
my @ cmdline = ( "./$module" , $ self - > { pbot } - > { interpreter } - > split_line ( $ args ) ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
my $ timeout = $ self - > { pbot } - > { registry } - > get_value ( 'general' , 'module_timeout' ) // 30 ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
my ( $ stdin , $ stdout , $ stderr ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
run \ @ cmdline , \ $ stdin , \ $ stdout , \ $ stderr , timeout ( $ timeout ) ;
2021-06-07 04:12:14 +02:00
2020-02-15 23:38:32 +01:00
my $ exitval = $? >> 8 ;
2021-06-07 04:12:14 +02:00
utf8:: decode $ stdout ;
utf8:: decode $ stderr ;
2020-02-15 23:38:32 +01:00
return ( $ exitval , $ stdout , $ stderr ) ;
} ;
if ( $@ ) {
my $ error = $@ ;
2021-02-07 22:41:22 +01:00
if ( $ error =~ m/timeout on timer/ ) {
( $ exitval , $ stdout , $ stderr ) = ( - 1 , "$context->{trigger}: timed-out" , '' ) ;
} else {
( $ exitval , $ stdout , $ stderr ) = ( - 1 , '' , $ error ) ;
2021-06-07 04:12:14 +02:00
$ self - > { pbot } - > { logger } - > log ( "$context->{trigger}: error executing module: $error\n" ) ;
2021-02-07 22:41:22 +01:00
}
2020-02-15 23:38:32 +01:00
}
if ( length $ stderr ) {
if ( open ( my $ fh , '>>' , "$module-stderr" ) ) {
print $ fh $ stderr ;
close $ fh ;
} else {
$ self - > { pbot } - > { logger } - > log ( "Failed to open $module-stderr: $!\n" ) ;
}
2020-02-15 03:52:41 +01:00
}
2020-05-02 05:59:51 +02:00
$ context - > { result } = $ stdout ;
chomp $ context - > { result } ;
2020-02-15 03:52:41 +01:00
}
1 ;