pbot/PBot/Commands.pm

345 lines
12 KiB
Perl
Raw Normal View History

# File: Commands.pm
#
# Purpose: Registers commands. Invokes commands with user capability
# validation.
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
package PBot::Commands;
use parent 'PBot::Class', 'PBot::Registerable';
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/;
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
# PBot::Commands can register subrefs
2020-02-15 23:38:32 +01:00
$self->PBot::Registerable::initialize(%conf);
# command metadata stored as a HashObject
$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;
# 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);
}
sub cmd_set {
my ($self, $context) = @_;
my ($command, $key, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3);
if (not defined $command) {
return "Usage: cmdset <command> [key [value]]";
}
return $self->{metadata}->set($command, $key, $value);
}
sub cmd_unset {
my ($self, $context) = @_;
my ($command, $key) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (not defined $command or not defined $key) {
return "Usage: cmdunset <command> <key>";
}
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)) {
# check for command metadata
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');
my $result = "/say $name: ";
# prefix help text with required capability
if ($requires_cap) {
$result .= "[Requires can-$keyword] ";
}
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;
}
return $result;
}
# no command metadata available
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});
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
my @factoids = $self->{pbot}->{factoids}->find_factoid($channel_arg, $keyword, exact_trigger => 1);
if (not @factoids or not $factoids[0]) {
# nothing found
return "I don't know anything about $keyword.";
}
my ($channel, $trigger);
if (@factoids > 1) {
# ask to disambiguate factoids if found in multiple channels
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]);
}
# get canonical channel and trigger names with original typographical casing
my $channel_name = $self->{pbot}->{factoids}->{storage}->get_key_name($channel);
my $trigger_name = $self->{pbot}->{factoids}->{storage}->get_key_name($channel, $trigger);
# 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\"";
}
# get factoid's `help` metadata
my $help = $self->{pbot}->{factoids}->{storage}->get_data($channel, $trigger, 'help');
# return immediately if no help text
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>`.";
}
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";
return $result;
}
sub register {
2020-02-15 23:38:32 +01:00
my ($self, $subref, $name, $requires_cap) = @_;
if (not defined $subref or not defined $name) {
Carp::croak("Missing parameters to Commands::register");
}
# register subref
my $command = $self->PBot::Registerable::register($subref);
# update internal metadata
$command->{name} = lc $name;
$command->{requires_cap} = $requires_cap // 0;
2020-02-15 23:38:32 +01:00
# update command metadata
if (not $self->{metadata}->exists($name)) {
$self->{metadata}->add($name, { requires_cap => $requires_cap, help => '' }, 1);
} else {
# metadata already exists, just update requires_cap unless it's already set.
if (not defined $self->get_meta($name, 'requires_cap')) {
$self->{metadata}->set($name, 'requires_cap', $requires_cap, 1);
}
}
2020-01-19 06:49:55 +01:00
# add can-<command> capability to PBot capabilities if required
if ($requires_cap) {
$self->{pbot}->{capabilities}->add("can-$name", undef, 1);
}
return $command;
}
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}};
}
sub exists {
2020-02-15 23:38:32 +01:00
my ($self, $keyword) = @_;
$keyword = lc $keyword;
foreach my $command (@{$self->{handlers}}) { return 1 if $command->{name} eq $keyword; }
2020-02-15 23:38:32 +01:00
return 0;
}
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);
}
# main entry point for PBot::Interpreter to interpret a registered bot command
# see also PBot::Factoids::interpreter() for factoid commands
sub interpreter {
2020-05-02 05:59:51 +02:00
my ($self, $context) = @_;
2020-02-15 23:38:32 +01: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
}
# 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
# 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
$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;
# 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
# check for a capability override
2020-02-15 23:38:32 +01:00
my $cap_override;
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
}
# 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) {
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
}
}
}
if ($self->get_meta($keyword, 'preserve_whitespace')) {
$context->{preserve_whitespace} = 1;
}
unless ($self->get_meta($keyword, 'dont-replace-pronouns')) {
$context->{arguments} = $self->{pbot}->{factoids}->expand_factoid_vars($context, $context->{arguments});
$context->{arglist} = $self->{pbot}->{interpreter}->make_args($context->{arguments});
}
# $self->{pbot}->{logger}->log("Disabling nickprefix\n");
#$context->{nickprefix_disabled} = 1;
2020-02-15 23:38:32 +01:00
if ($self->get_meta($keyword, 'background-process')) {
# 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,
sub { $context->{result} = $command->{subref}->($context) },
$timeout,
2020-02-15 23:38:32 +01:00
);
# return no output since it will be handled by process manager
return '';
2020-02-15 23:38:32 +01:00
} else {
# 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;
# return command output
2020-02-15 23:38:32 +01:00
return $result;
}
}
}
2020-02-15 23:38:32 +01:00
return undef;
}
1;