2010-03-22 08:33:44 +01:00
# File: Commands.pm
2020-02-02 07:17:20 +01:00
#
2020-02-03 18:50:38 +01:00
# Purpose: Registers commands. Invokes commands with user capability
# validation.
2010-03-22 08:33:44 +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-22 08:33:44 +01:00
package PBot::Commands ;
2020-02-08 20:04:13 +01:00
use parent 'PBot::Class' , 'PBot::Registerable' ;
2010-03-22 08:33:44 +01:00
2021-06-19 06:23:34 +02:00
use PBot::Imports ;
2019-07-11 03:40:53 +02:00
2020-01-24 06:09:57 +01:00
use Time::Duration qw/duration/ ;
2010-03-22 08:33:44 +01:00
sub initialize {
2020-02-15 23:38:32 +01:00
my ( $ self , % conf ) = @ _ ;
2021-06-09 00:52:47 +02:00
# PBot::Commands can register subrefs
2020-02-15 23:38:32 +01:00
$ self - > PBot::Registerable:: initialize ( % conf ) ;
2010-03-22 08:33:44 +01:00
2021-06-09 00:52:47 +02:00
# command metadata stored as a HashObject
2021-06-05 22:20:03 +02:00
$ self - > { metadata } = PBot::HashObject - > new ( pbot = > $ self - > { pbot } , name = > 'Command metadata' , filename = > $ conf { filename } ) ;
2020-02-15 23:38:32 +01:00
$ self - > { metadata } - > load ;
2010-03-22 08:33:44 +01:00
2021-06-05 22:20:03 +02:00
# register commands to manipulate command metadata and obtain help
$ self - > register ( sub { $ self - > cmd_set ( @ _ ) } , "cmdset" , 1 ) ;
$ self - > register ( sub { $ self - > cmd_unset ( @ _ ) } , "cmdunset" , 1 ) ;
$ self - > register ( sub { $ self - > cmd_help ( @ _ ) } , "help" , 0 ) ;
2021-05-29 22:57:11 +02:00
}
2020-05-04 22:21:35 +02:00
sub cmd_set {
my ( $ self , $ context ) = @ _ ;
2021-06-09 00:52:47 +02:00
2020-05-04 22:21:35 +02:00
my ( $ command , $ key , $ value ) = $ self - > { pbot } - > { interpreter } - > split_args ( $ context - > { arglist } , 3 ) ;
2021-06-09 00:52:47 +02:00
if ( not defined $ command ) {
return "Usage: cmdset <command> [key [value]]" ;
}
2020-05-04 22:21:35 +02:00
return $ self - > { metadata } - > set ( $ command , $ key , $ value ) ;
}
sub cmd_unset {
my ( $ self , $ context ) = @ _ ;
2021-06-09 00:52:47 +02:00
2020-05-04 22:21:35 +02:00
my ( $ command , $ key ) = $ self - > { pbot } - > { interpreter } - > split_args ( $ context - > { arglist } , 2 ) ;
2021-06-09 00:52:47 +02:00
2021-06-09 01:11:42 +02:00
if ( not defined $ command or not defined $ key ) {
2021-06-09 00:52:47 +02:00
return "Usage: cmdunset <command> <key>" ;
}
2020-05-04 22:21:35 +02:00
return $ self - > { metadata } - > unset ( $ command , $ key ) ;
}
sub cmd_help {
my ( $ self , $ context ) = @ _ ;
if ( not length $ context - > { arguments } ) {
return "For general help, see <https://github.com/pragma-/pbot/tree/master/doc>. For help about a specific command or factoid, use `help <keyword> [channel]`." ;
}
my $ keyword = lc $ self - > { pbot } - > { interpreter } - > shift_arg ( $ context - > { arglist } ) ;
# check built-in commands first
if ( $ self - > exists ( $ keyword ) ) {
2021-06-09 00:52:47 +02:00
# check for command metadata
2020-05-04 22:21:35 +02:00
if ( $ self - > { metadata } - > exists ( $ keyword ) ) {
my $ name = $ self - > { metadata } - > get_key_name ( $ keyword ) ;
my $ requires_cap = $ self - > { metadata } - > get_data ( $ keyword , 'requires_cap' ) ;
my $ help = $ self - > { metadata } - > get_data ( $ keyword , 'help' ) ;
2021-06-05 22:20:03 +02:00
my $ result = "/say $name: " ;
2021-06-09 00:52:47 +02:00
# prefix help text with required capability
if ( $ requires_cap ) {
$ result . = "[Requires can-$keyword] " ;
}
2020-05-04 22:21:35 +02:00
2021-06-05 22:20:03 +02:00
if ( not defined $ help or not length $ help ) {
$ result . = "I have no help text for this command yet. To add help text, use the command `cmdset $keyword help <text>`." ;
} else {
$ result . = $ help ;
}
2020-05-04 22:21:35 +02:00
return $ result ;
}
2021-06-05 22:20:03 +02:00
2021-06-09 00:52:47 +02:00
# no command metadata available
2020-05-04 22:21:35 +02:00
return "$keyword is a built-in command, but I have no help for it yet." ;
}
# then factoids
my $ channel_arg = $ self - > { pbot } - > { interpreter } - > shift_arg ( $ context - > { arglist } ) ;
2021-06-09 00:52:47 +02:00
if ( not defined $ channel_arg or not length $ channel_arg ) {
# set channel argument to from if no argument was passed
$ channel_arg = $ context - > { from } ;
}
if ( $ channel_arg !~ /^#/ ) {
# set channel argument to global if it's not channel-like
$ channel_arg = '.*' ;
}
# find factoids
2020-05-04 22:21:35 +02:00
my @ factoids = $ self - > { pbot } - > { factoids } - > find_factoid ( $ channel_arg , $ keyword , exact_trigger = > 1 ) ;
2021-06-09 00:52:47 +02:00
if ( not @ factoids or not $ factoids [ 0 ] ) {
# nothing found
return "I don't know anything about $keyword." ;
}
2020-05-04 22:21:35 +02:00
my ( $ channel , $ trigger ) ;
if ( @ factoids > 1 ) {
2021-06-09 00:52:47 +02:00
# ask to disambiguate factoids if found in multiple channels
2020-05-04 22:21:35 +02:00
if ( not grep { $ _ - > [ 0 ] eq $ channel_arg } @ factoids ) {
return
"/say $keyword found in multiple channels: "
. ( join ', ' , sort map { $ _ - > [ 0 ] eq '.*' ? 'global' : $ _ - > [ 0 ] } @ factoids )
. "; use `help $keyword <channel>` to disambiguate." ;
} else {
foreach my $ factoid ( @ factoids ) {
if ( $ factoid - > [ 0 ] eq $ channel_arg ) {
( $ channel , $ trigger ) = ( $ factoid - > [ 0 ] , $ factoid - > [ 1 ] ) ;
last ;
}
}
}
} else {
( $ channel , $ trigger ) = ( $ factoids [ 0 ] - > [ 0 ] , $ factoids [ 0 ] - > [ 1 ] ) ;
}
2021-06-09 00:52:47 +02:00
# get canonical channel and trigger names with original typographical casing
2021-07-09 23:39:35 +02:00
my $ channel_name = $ self - > { pbot } - > { factoids } - > { storage } - > get_key_name ( $ channel ) ;
my $ trigger_name = $ self - > { pbot } - > { factoids } - > { storage } - > get_key_name ( $ channel , $ trigger ) ;
2020-05-04 22:21:35 +02:00
2021-06-09 00:52:47 +02:00
# prettify channel name if it's ".*"
if ( $ channel_name eq '.*' ) {
$ channel_name = 'global channel' ;
}
# prettify trigger name with double-quotes if it contains spaces
if ( $ trigger_name =~ / / ) {
$ trigger_name = "\"$trigger_name\"" ;
}
2020-05-04 22:21:35 +02:00
2021-06-09 00:52:47 +02:00
# get factoid's `help` metadata
2021-07-09 23:39:35 +02:00
my $ help = $ self - > { pbot } - > { factoids } - > { storage } - > get_data ( $ channel , $ trigger , 'help' ) ;
2020-05-04 22:21:35 +02:00
2021-06-09 00:52:47 +02:00
# return immediately if no help text
2021-06-05 22:20:03 +02:00
if ( not defined $ help or not length $ help ) {
return "/say $trigger_name is a factoid for $channel_name, but I have no help text for it yet."
. " To add help text, use the command `factset $trigger_name help <text>`." ;
}
2020-05-04 22:21:35 +02:00
2021-06-09 00:52:47 +02:00
my $ result = "/say " ;
# if factoid doesn't belong to invoked or global channel,
# then prefix with the factoid's channel name.
if ( $ channel ne $ context - > { from } and $ channel ne '.*' ) {
$ result . = "[$channel_name] " ;
}
$ result . = "$trigger_name: $help" ;
2020-05-04 22:21:35 +02:00
return $ result ;
}
2010-03-22 08:33:44 +01:00
sub register {
2020-02-15 23:38:32 +01:00
my ( $ self , $ subref , $ name , $ requires_cap ) = @ _ ;
2021-06-09 00:52:47 +02:00
if ( not defined $ subref or not defined $ name ) {
Carp:: croak ( "Missing parameters to Commands::register" ) ;
}
# register subref
2021-06-11 23:58:16 +02:00
my $ command = $ self - > PBot::Registerable:: register ( $ subref ) ;
2021-06-09 00:52:47 +02:00
# update internal metadata
2021-06-11 23:58:16 +02:00
$ command - > { name } = lc $ name ;
$ command - > { requires_cap } = $ requires_cap // 0 ;
2020-02-15 23:38:32 +01:00
2021-06-09 00:52:47 +02:00
# update command metadata
2021-06-05 22:20:03 +02:00
if ( not $ self - > { metadata } - > exists ( $ name ) ) {
2021-06-09 00:52:47 +02:00
$ self - > { metadata } - > add ( $ name , { requires_cap = > $ requires_cap , help = > '' } , 1 ) ;
2021-06-05 22:20:03 +02:00
} else {
2021-06-09 00:52:47 +02:00
# metadata already exists, just update requires_cap unless it's already set.
2021-06-05 22:20:03 +02:00
if ( not defined $ self - > get_meta ( $ name , 'requires_cap' ) ) {
$ self - > { metadata } - > set ( $ name , 'requires_cap' , $ requires_cap , 1 ) ;
}
2020-01-19 07:13:08 +01:00
}
2020-01-19 06:49:55 +01:00
2021-06-09 00:52:47 +02:00
# add can-<command> capability to PBot capabilities if required
if ( $ requires_cap ) {
$ self - > { pbot } - > { capabilities } - > add ( "can-$name" , undef , 1 ) ;
}
2021-06-11 23:58:16 +02:00
return $ command ;
2010-03-22 08:33:44 +01:00
}
2016-02-14 03:38:43 +01:00
sub unregister {
2020-02-15 23:38:32 +01:00
my ( $ self , $ name ) = @ _ ;
Carp:: croak ( "Missing name parameter to Commands::unregister" ) if not defined $ name ;
$ name = lc $ name ;
@ { $ self - > { handlers } } = grep { $ _ - > { name } ne $ name } @ { $ self - > { handlers } } ;
2010-03-22 08:33:44 +01:00
}
2015-04-04 00:33:19 +02:00
sub exists {
2020-02-15 23:38:32 +01:00
my ( $ self , $ keyword ) = @ _ ;
$ keyword = lc $ keyword ;
2021-06-11 23:58:16 +02:00
foreach my $ command ( @ { $ self - > { handlers } } ) { return 1 if $ command - > { name } eq $ keyword ; }
2020-02-15 23:38:32 +01:00
return 0 ;
2015-04-04 00:33:19 +02:00
}
2020-05-04 22:21:35 +02:00
sub set_meta {
my ( $ self , $ command , $ key , $ value , $ save ) = @ _ ;
return undef if not $ self - > { metadata } - > exists ( $ command ) ;
$ self - > { metadata } - > set ( $ command , $ key , $ value , ! $ save ) ;
return 1 ;
}
sub get_meta {
my ( $ self , $ command , $ key ) = @ _ ;
return $ self - > { metadata } - > get_data ( $ command , $ key ) ;
}
2021-06-09 00:52:47 +02:00
# main entry point for PBot::Interpreter to interpret a registered bot command
# see also PBot::Factoids::interpreter() for factoid commands
2010-03-22 08:33:44 +01:00
sub interpreter {
2020-05-02 05:59:51 +02:00
my ( $ self , $ context ) = @ _ ;
2020-02-15 23:38:32 +01:00
2021-06-09 00:52:47 +02:00
# debug flag to trace $context location and contents
2020-02-15 23:38:32 +01:00
if ( $ self - > { pbot } - > { registry } - > get_value ( 'general' , 'debugcontext' ) ) {
use Data::Dumper ;
$ Data:: Dumper:: Sortkeys = 1 ;
$ self - > { pbot } - > { logger } - > log ( "Commands::interpreter\n" ) ;
2020-05-02 05:59:51 +02:00
$ self - > { pbot } - > { logger } - > log ( Dumper $ context ) ;
2020-02-15 23:38:32 +01:00
}
2021-06-11 23:58:16 +02:00
# some convenient aliases
2020-05-02 05:59:51 +02:00
my $ keyword = lc $ context - > { keyword } ;
my $ from = $ context - > { from } ;
2020-02-15 23:38:32 +01:00
2021-06-11 23:58:16 +02:00
# set the channel the command is in reference to
2020-05-02 05:59:51 +02:00
my ( $ cmd_channel ) = $ context - > { arguments } =~ m/\B(#[^ ]+)/ ; # assume command is invoked in regards to first channel-like argument
2021-06-05 22:20:03 +02:00
$ cmd_channel = $ from if not defined $ cmd_channel ; # otherwise command is invoked in regards to the channel the user is in
$ context - > { channel } = $ cmd_channel ;
2021-06-11 23:58:16 +02:00
# get the user's bot account
my $ user = $ self - > { pbot } - > { users } - > find_user ( $ cmd_channel , $ context - > { hostmask } ) ;
2020-02-15 23:38:32 +01:00
2021-06-11 23:58:16 +02:00
# check for a capability override
2020-02-15 23:38:32 +01:00
my $ cap_override ;
2021-06-11 23:58:16 +02:00
2020-05-02 05:59:51 +02:00
if ( exists $ context - > { 'cap-override' } ) {
$ self - > { pbot } - > { logger } - > log ( "Override cap to $context->{'cap-override'}\n" ) ;
$ cap_override = $ context - > { 'cap-override' } ;
2020-02-15 23:38:32 +01:00
}
2021-06-11 23:58:16 +02:00
# go through all commands
# TODO: maybe use a hash lookup
foreach my $ command ( @ { $ self - > { handlers } } ) {
# is this the command
if ( $ command - > { name } eq $ keyword ) {
# does this command require capabilities
my $ requires_cap = $ self - > get_meta ( $ keyword , 'requires_cap' ) // $ command - > { requires_cap } ;
2020-02-15 23:38:32 +01:00
if ( $ requires_cap ) {
if ( defined $ cap_override ) {
if ( not $ self - > { pbot } - > { capabilities } - > has ( $ cap_override , "can-$keyword" ) ) {
2020-05-02 05:59:51 +02:00
return "/msg $context->{nick} The $keyword command requires the can-$keyword capability, which cap-override $cap_override does not have." ;
2020-02-15 23:38:32 +01:00
}
} else {
if ( not defined $ user ) {
2021-06-11 23:58:16 +02:00
my ( $ found_chan , $ found_mask ) = $ self - > { pbot } - > { users } - > find_user_account ( $ cmd_channel , $ context - > { hostmask } , 1 ) ;
if ( not defined $ found_chan ) {
return "/msg $context->{nick} You must have a user account to use $keyword. You may use the `my` command to create a personal user account. See `help my`." ;
} else {
return "/msg $context->{nick} You must have a user account in $cmd_channel to use $keyword. (You have an account in $found_chan.)" ;
}
2020-02-15 23:38:32 +01:00
} elsif ( not $ user - > { loggedin } ) {
2020-05-02 05:59:51 +02:00
return "/msg $context->{nick} You must be logged into your user account to use $keyword." ;
2020-02-15 23:38:32 +01:00
}
if ( not $ self - > { pbot } - > { capabilities } - > userhas ( $ user , "can-$keyword" ) ) {
2020-05-02 05:59:51 +02:00
return "/msg $context->{nick} The $keyword command requires the can-$keyword capability, which your user account does not have." ;
2020-02-15 23:38:32 +01:00
}
}
2020-02-10 23:42:29 +01:00
}
2020-02-03 18:50:38 +01:00
2020-07-24 22:10:54 +02:00
if ( $ self - > get_meta ( $ keyword , 'preserve_whitespace' ) ) {
$ context - > { preserve_whitespace } = 1 ;
}
2020-05-22 04:57:11 +02:00
unless ( $ self - > get_meta ( $ keyword , 'dont-replace-pronouns' ) ) {
2020-06-21 05:55:22 +02:00
$ context - > { arguments } = $ self - > { pbot } - > { factoids } - > expand_factoid_vars ( $ context , $ context - > { arguments } ) ;
2021-06-11 23:58:16 +02:00
$ context - > { arglist } = $ self - > { pbot } - > { interpreter } - > make_args ( $ context - > { arguments } ) ;
2020-05-22 04:57:11 +02:00
}
2020-03-18 07:56:44 +01:00
2021-07-09 23:39:35 +02:00
# $self->{pbot}->{logger}->log("Disabling nickprefix\n");
#$context->{nickprefix_disabled} = 1;
2021-06-11 23:58:16 +02:00
2020-02-15 23:38:32 +01:00
if ( $ self - > get_meta ( $ keyword , 'background-process' ) ) {
2021-06-11 23:58:16 +02:00
# execute this command as a backgrounded process
# set timeout to command metadata value
my $ timeout = $ self - > get_meta ( $ keyword , 'process-timeout' ) ;
# otherwise set timeout to default value
$ timeout // = $ self - > { pbot } - > { registry } - > get_value ( 'processmanager' , 'default_timeout' ) ;
# execute command in background
2020-02-15 23:38:32 +01:00
$ self - > { pbot } - > { process_manager } - > execute_process (
2020-05-02 05:59:51 +02:00
$ context ,
2021-06-11 23:58:16 +02:00
sub { $ context - > { result } = $ command - > { subref } - > ( $ context ) } ,
$ timeout ,
2020-02-15 23:38:32 +01:00
) ;
2021-06-11 23:58:16 +02:00
# return no output since it will be handled by process manager
return '' ;
2020-02-15 23:38:32 +01:00
} else {
2021-06-11 23:58:16 +02:00
# execute this command normally
my $ result = $ command - > { subref } - > ( $ context ) ;
# disregard undesired command output if command is embedded
2020-05-02 05:59:51 +02:00
return undef if $ context - > { referenced } and $ result =~ m/(?:usage:|no results)/i ;
2021-06-11 23:58:16 +02:00
# return command output
2020-02-15 23:38:32 +01:00
return $ result ;
}
2010-03-22 08:33:44 +01:00
}
}
2021-06-11 23:58:16 +02:00
2020-02-15 23:38:32 +01:00
return undef ;
2010-03-22 08:33:44 +01:00
}
1 ;