3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-26 13:59:47 +01:00
pbot/lib/PBot/Core/Factoids.pm

1505 lines
56 KiB
Perl
Raw Normal View History

# File: Factoids.pm
#
2021-06-19 06:23:34 +02:00
# Purpose: Provides functionality for factoids.
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
2021-07-21 07:44:51 +02:00
package PBot::Core::Factoids;
use parent 'PBot::Core::Class';
2021-06-19 06:23:34 +02:00
use PBot::Imports;
use HTML::Entities;
use Time::HiRes qw(gettimeofday);
use Time::Duration qw(duration);
use POSIX qw(strftime);
use Text::ParseWords;
use JSON;
2021-07-24 04:22:25 +02:00
use PBot::Core::Utils::Indefinite;
use PBot::Core::Utils::ValidateString;
our %factoid_metadata = (
'action' => 'TEXT',
'action_with_args' => 'TEXT',
'add_nick' => 'INTEGER',
'allow_empty_args' => 'INTEGER',
'background-process' => 'INTEGER',
'cap-override' => 'TEXT',
'created_on' => 'NUMERIC',
'dont-protect-self' => 'INTEGER',
'dont-replace-pronouns' => 'INTEGER',
'edited_by' => 'TEXT',
'edited_on' => 'NUMERIC',
'enabled' => 'INTEGER',
'help' => 'TEXT',
'interpolate' => 'INTEGER',
'keyword_override' => 'TEXT',
'last_referenced_in' => 'TEXT',
'last_referenced_on' => 'NUMERIC',
'locked' => 'INTEGER',
'locked_to_channel' => 'INTEGER',
'no_keyword_override' => 'INTEGER',
'noembed' => 'INTEGER',
'nooverride' => 'INTEGER',
'owner' => 'TEXT',
'persist-key' => 'INTEGER',
'preserve_whitespace' => 'INTEGER',
'process-timeout' => 'INTEGER',
'rate_limit' => 'INTEGER',
'ref_count' => 'INTEGER',
'ref_user' => 'TEXT',
'require_explicit_args' => 'INTEGER',
'requires_arguments' => 'INTEGER',
'type' => 'TEXT',
'unquote_spaces' => 'INTEGER',
'usage' => 'TEXT',
'use_output_queue' => 'INTEGER',
'workdir' => 'TEXT',
);
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
my $filename = $conf{filename};
$self->{pbot} = $self->{pbot};
2021-07-24 04:22:25 +02:00
$self->{storage} = PBot::Core::Storage::DualIndexSQLiteObject->new(
2021-07-24 03:26:45 +02:00
pbot => $self->{pbot},
name => 'Factoids',
filename => $filename,
);
2020-02-15 23:38:32 +01:00
$self->{pbot}->{registry}->add_default('text', 'factoids', 'default_rate_limit', 15);
$self->{pbot}->{registry}->add_default('text', 'factoids', 'max_name_length', 100);
$self->{pbot}->{registry}->add_default('text', 'factoids', 'max_content_length', 1024 * 8);
$self->{pbot}->{registry}->add_default('text', 'factoids', 'max_channel_length', 20);
2020-02-15 23:38:32 +01:00
$self->load_factoids;
# save and export factoids at exit
$self->{pbot}->{atexit}->register(sub { $self->save_factoids(1) });
}
sub load_factoids {
my ($self) = @_;
$self->{storage}->load;
$self->{storage}->create_metadata(\%factoid_metadata);
}
sub save_factoids {
my ($self, $export) = @_;
$self->{storage}->save;
$self->export_factoids if $export;
}
2020-02-10 10:06:38 +01:00
sub get_meta {
2020-02-15 23:38:32 +01:00
my ($self, $channel, $trigger, $key) = @_;
$channel = lc $channel;
$trigger = lc $trigger;
my ($chan, $trig) = $self->find_factoid($channel, $trigger, exact_channel => 1, exact_trigger => 1);
2020-02-15 23:38:32 +01:00
return undef if not defined $chan;
return $self->{storage}->get_data($chan, $trig, $key);
2020-02-10 10:06:38 +01:00
}
sub add_factoid {
2020-07-18 21:46:44 +02:00
my ($self, $type, $channel, $owner, $trigger, $action, $dont_save) = @_;
2020-02-15 23:38:32 +01:00
$type = lc $type;
$channel = '.*' if $channel !~ /^#/;
my $data;
if ($self->{storage}->exists($channel, $trigger)) {
2020-02-15 23:38:32 +01:00
# only update action field if force-adding it through factadd -f
$data = $self->{storage}->get_data($channel, $trigger);
2020-02-15 23:38:32 +01:00
$data->{action} = $action;
$data->{type} = $type;
} else {
$data = {
enabled => 1,
type => $type,
action => $action,
owner => $owner,
created_on => scalar gettimeofday,
ref_count => 0,
ref_user => "nobody",
rate_limit => $self->{pbot}->{registry}->get_value('factoids', 'default_rate_limit')
};
}
$self->{storage}->add($channel, $trigger, $data, $dont_save);
$self->{pbot}->{commands}->{modules}->{Factoids}->log_factoid($channel, $trigger, $owner, "created: $action") unless $dont_save;
}
sub remove_factoid {
2020-02-15 23:38:32 +01:00
my $self = shift;
my ($channel, $trigger) = @_;
$channel = '.*' if $channel !~ /^#/;
return $self->{storage}->remove($channel, $trigger);
}
sub export_factoids {
2020-02-15 23:38:32 +01:00
my $self = shift;
my $filename;
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
if (@_) { $filename = shift; }
else { $filename = $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/factoids.html'; }
return if not defined $filename;
return if not defined $self->{storage}->{dbh};
$self->{pbot}->{logger}->log("Exporting factoids to $filename\n");
2020-02-15 23:38:32 +01:00
open FILE, "> $filename" or return "Could not open export path.";
2020-02-15 23:38:32 +01:00
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
my $time = localtime;
print FILE "<html><head>\n<link href='css/blue.css' rel='stylesheet' type='text/css'>\n";
print FILE '<script type="text/javascript" src="js/jquery-latest.js"></script>' . "\n";
print FILE '<script type="text/javascript" src="js/jquery.tablesorter.js"></script>' . "\n";
print FILE '<script type="text/javascript" src="js/picnet.table.filter.min.js"></script>' . "\n";
print FILE "</head>\n<body><i>Last updated at $time</i>\n";
print FILE "<hr><h2>$botnick\'s factoids</h2>\n";
2020-02-15 23:38:32 +01:00
my $i = 0;
my $table_id = 1;
foreach my $channel (sort $self->{storage}->get_keys) {
next if not $self->{storage}->get_keys($channel);
my $chan = $self->{storage}->get_data($channel, '_name');
2020-02-15 23:38:32 +01:00
$chan = 'global' if $chan eq '.*';
2020-02-15 23:38:32 +01:00
print FILE "<a href='#" . encode_entities($chan) . "'>" . encode_entities($chan) . "</a><br>\n";
}
foreach my $channel (sort $self->{storage}->get_keys) {
next if not $self->{storage}->get_keys($channel);
my $chan = $self->{storage}->get_data($channel, '_name');
2020-02-15 23:38:32 +01:00
$chan = 'global' if $chan eq '.*';
print FILE "<a name='" . encode_entities($chan) . "'></a>\n";
print FILE "<hr>\n<h3>" . encode_entities($chan) . "</h3>\n<hr>\n";
print FILE "<table border=\"0\" id=\"table$table_id\" class=\"tablesorter\">\n";
print FILE "<thead>\n<tr>\n";
print FILE "<th>owner</th>\n";
print FILE "<th>created on</th>\n";
print FILE "<th>times referenced</th>\n";
print FILE "<th>factoid</th>\n";
print FILE "<th>last edited by</th>\n";
print FILE "<th>edited date</th>\n";
print FILE "<th>last referenced by</th>\n";
print FILE "<th>last referenced date</th>\n";
print FILE "</tr>\n</thead>\n<tbody>\n";
$table_id++;
my $iter = $self->{storage}->get_each("index1 = $channel", '_everything', '_sort = index1');
while (defined (my $factoid = $self->{storage}->get_next($iter))) {
my $trigger_name = $self->{storage}->get_data($factoid->{index1}, $factoid->{index2}, '_name');
if ($factoid->{type} eq 'text') {
2020-02-15 23:38:32 +01:00
$i++;
if ($i % 2) { print FILE "<tr bgcolor=\"#dddddd\">\n"; }
else { print FILE "<tr>\n"; }
print FILE "<td>" . encode_entities($factoid->{'owner'}) . "</td>\n";
print FILE "<td>" . encode_entities(strftime "%Y/%m/%d %H:%M:%S", localtime $factoid->{'created_on'}) . "</td>\n";
2020-02-15 23:38:32 +01:00
print FILE "<td>" . $factoid->{'ref_count'} . "</td>\n";
2020-02-15 23:38:32 +01:00
my $action = $factoid->{'action'};
2020-02-15 23:38:32 +01:00
if ($action =~ m/https?:\/\/[^ ]+/) {
$action =~ s/(.*?)http(s?:\/\/[^ ]+)/encode_entities($1) . "<a href='http" . encode_entities($2) . "'>http" . encode_entities($2) . "<\/a>"/ge;
$action =~ s/(.*)<\/a>(.*$)/"$1<\/a>" . encode_entities($2)/e;
} else {
$action = encode_entities($action);
}
if (defined $factoid->{'action_with_args'}) {
my $with_args = $factoid->{'action_with_args'};
2020-02-15 23:38:32 +01:00
$with_args =~ s/(.*?)http(s?:\/\/[^ ]+)/encode_entities($1) . "<a href='http" . encode_entities($2) . "'>http" . encode_entities($2) . "<\/a>"/ge;
$with_args =~ s/(.*)<\/a>(.*$)/"$1<\/a>" . encode_entities($2)/e;
print FILE "<td width=100%><b>" . encode_entities($trigger_name) . "</b> is $action<br><br><b>with_args:</b> " . encode_entities($with_args) . "</td>\n";
} else {
print FILE "<td width=100%><b>" . encode_entities($trigger_name) . "</b> is $action</td>\n";
}
if (defined $factoid->{'edited_by'}) {
print FILE "<td>" . $factoid->{'edited_by'} . "</td>\n";
print FILE "<td>" . encode_entities(strftime "%Y/%m/%d %H:%M:%S", localtime $factoid->{'edited_on'}) . "</td>\n";
2020-02-15 23:38:32 +01:00
} else {
print FILE "<td></td>\n";
print FILE "<td></td>\n";
}
print FILE "<td>" . encode_entities($factoid->{'ref_user'}) . "</td>\n";
2020-02-15 23:38:32 +01:00
if (defined $factoid->{'last_referenced_on'}) {
print FILE "<td>" . encode_entities(strftime "%Y/%m/%d %H:%M:%S", localtime $factoid->{'last_referenced_on'}) . "</td>\n";
2020-02-15 23:38:32 +01:00
} else {
print FILE "<td></td>\n";
}
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
print FILE "</tr>\n";
}
}
print FILE "</tbody>\n</table>\n";
}
2020-02-15 23:38:32 +01:00
print FILE "<hr>$i factoids memorized.<br>";
print FILE "<hr><i>Last updated at $time</i>\n";
print FILE "<script type='text/javascript'>\n";
$table_id--;
2020-02-15 23:38:32 +01:00
print FILE '$(document).ready(function() {' . "\n";
while ($table_id > 0) {
print FILE '$("#table' . $table_id . '").tablesorter();' . "\n";
print FILE '$("#table' . $table_id . '").tableFilter();' . "\n";
$table_id--;
}
print FILE "});\n";
print FILE "</script>\n";
print FILE "</body>\n</html>\n";
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
close(FILE);
2019-06-26 18:34:19 +02:00
2020-02-15 23:38:32 +01:00
return "/say $i factoids exported.";
}
2010-04-02 19:33:18 +02:00
sub find_factoid {
2020-02-15 23:38:32 +01:00
my ($self, $from, $keyword, %opts) = @_;
2020-02-15 23:38:32 +01:00
my %default_opts = (
arguments => '',
exact_channel => 0,
exact_trigger => 0,
find_alias => 0
);
2020-02-15 23:38:32 +01:00
%opts = (%default_opts, %opts);
my $debug = 0;
if ($debug) {
use Data::Dumper;
my $dump = Dumper \%opts;
2020-02-26 11:32:22 +01:00
$self->{pbot}->{logger}->log("+" x 32 . "\n");
use Devel::StackTrace;
2021-07-21 07:44:51 +02:00
my $trace = Devel::StackTrace->new(indent => 1, ignore_class => ['PBot::PBot', 'PBot::Core::IRC']);
2020-02-26 11:32:22 +01:00
$self->{pbot}->{logger}->log("find_factoid stacktrace: " . $trace->as_string() . "\n");
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("find_factiod: from: $from, kw: $keyword, opts: $dump\n");
}
$from = '.*' if not defined $from or $from !~ /^#/;
$from = lc $from;
$keyword = lc $keyword;
my $arguments = $opts{arguments};
my @result = eval {
my @results;
2020-02-26 11:32:22 +01:00
my ($channel, $trigger);
for (my $depth = 0; $depth < 15; $depth++) {
my $action;
2020-02-15 23:38:32 +01:00
my $string = $keyword . (length $arguments ? " $arguments" : "");
$self->{pbot}->{logger}->log("string: $string\n") if $debug;
2020-02-26 11:32:22 +01:00
if ($opts{exact_channel} and $opts{exact_trigger}) {
if ($self->{storage}->exists($from, $keyword)) {
2020-02-26 11:32:22 +01:00
($channel, $trigger) = ($from, $keyword);
goto CHECK_ALIAS;
}
if ($opts{exact_trigger} > 1 and $self->{storage}->exists('.*', $keyword)) {
2020-02-26 11:32:22 +01:00
($channel, $trigger) = ('.*', $keyword);
goto CHECK_ALIAS;
}
goto CHECK_REGEX;
}
if ($opts{exact_channel} and not $opts{exact_trigger}) {
if (not $self->{storage}->exists($from, $keyword)) {
($channel, $trigger) = ($from, $keyword);
2020-02-26 11:32:22 +01:00
goto CHECK_REGEX if $from eq '.*';
goto CHECK_REGEX if not $self->{storage}->exists('.*', $keyword);
2020-02-26 11:32:22 +01:00
($channel, $trigger) = ('.*', $keyword);
goto CHECK_ALIAS;
}
($channel, $trigger) = ($from, $keyword);
goto CHECK_ALIAS;
}
if (not $opts{exact_channel}) {
foreach my $factoid ($self->{storage}->get_all("index2 = $keyword", 'index1', 'action')) {
2020-02-26 11:32:22 +01:00
$channel = $factoid->{index1};
$trigger = $keyword;
2020-02-26 11:32:22 +01:00
if ($opts{find_alias} && $factoid->{action} =~ m{^/call\s+(.*)$}ms) {
2020-02-26 11:32:22 +01:00
goto CHECK_ALIAS;
}
push @results, [$channel, $trigger];
}
goto CHECK_REGEX;
}
CHECK_ALIAS:
if ($opts{find_alias}) {
$action = $self->{storage}->get_data($channel, $trigger, 'action') if not defined $action;
2020-02-26 11:32:22 +01:00
if ($action =~ m{^/call\s+(.*)$}ms) {
my $command;
if (length $arguments) {
$command = "$1 $arguments";
} else {
$command = $1;
}
my $arglist = $self->{pbot}->{interpreter}->make_args($command);
($keyword, $arguments) = $self->{pbot}->{interpreter}->split_args($arglist, 2, 0, 1);
goto NEXT_DEPTH;
}
}
if ($opts{exact_channel} == 1) {
return ($channel, $trigger);
} else {
push @results, [$channel, $trigger];
}
CHECK_REGEX:
if (not $opts{exact_trigger}) {
my @factoids;
2020-02-26 11:32:22 +01:00
2020-02-15 23:38:32 +01:00
if ($opts{exact_channel}) {
if ($channel ne '.*') {
@factoids = $self->{storage}->get_all('type = regex', "index1 = $channel", 'OR index1 = .*', 'index2', 'action');
} else {
@factoids = $self->{storage}->get_all('type = regex', "index1 = $channel", 'index2', 'action');
}
2020-02-26 11:32:22 +01:00
} else {
@factoids = $self->{storage}->get_all('type = regex', 'index1', 'index2', 'action');
}
foreach my $factoid (@factoids) {
$channel = $factoid->{index1};
$trigger = $factoid->{index2};
$action = $factoid->{action};
if ($string =~ /$trigger/) {
2020-02-26 11:32:22 +01:00
if ($opts{find_alias}) {
my $command = $action;
2020-02-15 23:38:32 +01:00
my $arglist = $self->{pbot}->{interpreter}->make_args($command);
($keyword, $arguments) = $self->{pbot}->{interpreter}->split_args($arglist, 2, 0, 1);
goto NEXT_DEPTH;
}
if ($opts{exact_channel} == 1) { return ($channel, $trigger); }
else { push @results, [$channel, $trigger]; }
}
}
}
2020-02-26 11:32:22 +01:00
# match not found
last;
2020-02-15 23:38:32 +01:00
NEXT_DEPTH:
last if not $opts{find_alias};
}
2020-02-15 23:38:32 +01:00
if ($debug) {
if (not @results) { $self->{pbot}->{logger}->log("find_factoid: no match\n"); }
else {
$self->{pbot}->{logger}->log("find_factoid: got results: " . (join ', ', map { "$_->[0] -> $_->[1]" } @results) . "\n");
}
}
return @results;
};
2010-04-02 19:33:18 +02:00
2020-02-15 23:38:32 +01:00
if ($@) {
2020-02-26 11:32:22 +01:00
$self->{pbot}->{logger}->log("Error in find_factoid: $@\n");
2020-02-15 23:38:32 +01:00
return undef;
}
2010-04-02 19:33:18 +02:00
2020-02-15 23:38:32 +01:00
return @result;
}
sub parse_expansion_modifiers {
my ($self, $modifier) = @_;
my %settings;
while ($$modifier =~ s/^:(?=\w)//) {
if ($$modifier =~ s/^join\s*(?=\(.*?(?=\)))//) {
my ($params, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($$modifier, '(', ')', '', 1);
$$modifier = $rest;
my @args = $self->{pbot}->{interpreter}->split_line($params, strip_quotes => 1, strip_commas => 1);
$settings{'join'} = $args[0];
next;
}
if ($$modifier=~ s/^\+?sort//) {
$settings{'sort+'} = 1;
next;
}
if ($$modifier=~ s/^\-sort//) {
$settings{'sort-'} = 1;
next;
}
if ($$modifier=~ s/^pick_unique\s*(?=\(.*?(?=\)))//) {
my ($params, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($$modifier, '(', ')', '', 1);
$$modifier = $rest;
my @args = $self->{pbot}->{interpreter}->split_line($params, strip_quotes => 1, strip_commas => 1);
$settings{'pick'} = 1;
$settings{'unique'} = 1;
if (@args == 2) {
$settings{'random'} = 1;
$settings{'pick_min'} = $args[0];
$settings{'pick_max'} = $args[1];
} elsif (@args == 1) {
$settings{'pick_min'} = 1;
$settings{'pick_max'} = $args[0];
} else {
push @{$settings{errors}}, "pick_unique(): missing argument(s)";
}
next;
}
if ($$modifier=~ s/^pick\s*(?=\(.*?(?=\)))//) {
my ($params, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($$modifier, '(', ')', '', 1);
$$modifier = $rest;
my @args = $self->{pbot}->{interpreter}->split_line($params, strip_quotes => 1, strip_commas => 1);
$settings{'pick'} = 1;
if (@args == 2) {
$settings{'random'} = 1;
$settings{'pick_min'} = $args[0];
$settings{'pick_max'} = $args[1];
} elsif (@args == 1) {
$settings{'pick_min'} = 1;
$settings{'pick_max'} = $args[0];
} else {
push @{$settings{errors}}, "pick(): missing argument(s)";
}
next;
}
if ($$modifier=~ s/^index\s*(?=\(.*?(?=\)))//) {
my ($params, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($$modifier, '(', ')', '', 1);
$$modifier = $rest;
my @args = $self->{pbot}->{interpreter}->split_line($params, strip_quotes => 1, strip_commas => 1);
if (@args == 1) {
$settings{'index'} = $args[0];
} else {
push @{$settings{errors}}, "index(): missing argument";
}
next;
}
2020-07-18 21:46:44 +02:00
if ($$modifier =~ s/^(enumerate|comma|ucfirst|lcfirst|title|uc|lc)//) {
$settings{$1} = 1;
next;
}
if ($$modifier =~ s/^(\w+)//) {
push @{$settings{errors}}, "Unknown modifier `$1`";
}
}
return %settings;
}
sub make_list {
my ($self, $context, $extracted, $settings, %opts) = @_;
if ($extracted =~ /(.*?)(?<!\\)%\s*\(.*\)/) {
2020-06-08 00:22:48 +02:00
$opts{nested}++;
$extracted = $self->expand_factoid_selectors($context, $extracted, %opts);
2020-06-08 00:22:48 +02:00
$opts{nested}--;
}
my @list;
foreach my $item (split /\s*(?<!\\)\|\s*/, $extracted, -1) {
$item =~ s/^\s+|\s+$//g;
$item =~ s/\\\|/|/g;
2020-06-08 00:22:48 +02:00
if ($settings->{'uc'}) { $item = uc $item; }
2020-06-08 00:22:48 +02:00
if ($settings->{'lc'}) { $item = lc $item; }
2020-06-08 00:22:48 +02:00
if ($settings->{'ucfirst'}) { $item = ucfirst $item; }
2020-06-08 00:22:48 +02:00
if ($settings->{'title'}) {
$item = ucfirst lc $item;
$item =~ s/ (\w)/' ' . uc $1/ge;
}
2020-06-08 00:22:48 +02:00
if ($settings->{'json'}) { $item = $self->escape_json($item); }
push @list, $item;
}
if ($settings->{'unique'}) {
foreach my $choice (@{$settings->{'choices'}}) {
@list = grep { $_ ne $choice } @list;
}
}
if ($settings->{'sort+'}) {
@list = sort { $a cmp $b } @list;
}
if ($settings->{'sort-'}) {
@list = sort { $b cmp $a } @list;
}
return \@list;
}
sub select_weighted_item_from_list {
my ($self, $list, $index) = @_;
my @weights;
my $weight_sum = 0;
for (my $i = 0; $i <= $#$list; $i++) {
my $weight = 1;
if ($list->[$i] =~ s/:weight\(([0-9.-]+)\)//) {
$weight = $1;
}
$weights[$i] = [ $weight, $i ];
$weight_sum += $weight;
}
if (defined $index) {
return $list->[$index];
}
my $n = rand $weight_sum;
for my $weight (@weights) {
if ($n < $weight->[0]) {
return $list->[$weight->[1]];
}
$n -= $weight->[0];
}
}
sub select_item {
my ($self, $context, $extracted, $modifier, %opts) = @_;
my %settings = $self->parse_expansion_modifiers($modifier);
if (exists $settings{errors}) {
return "[Error: " . join ('; ', @{$settings{errors}}) . ']';
}
my $item;
if (exists $settings{'index'}) {
my $list = $self->make_list($context, $extracted, \%settings, %opts);
my $index = $settings{'index'};
$index = $#$list - -$index if $index < 0;
$index = 0 if $index < 0;
$index = $#$list if $index > $#$list;
$item = $self->select_weighted_item_from_list($list, $index);
# strip outer quotes
if (not $item =~ s/^"(.*)"$/$1/) { $item =~ s/^'(.*)'$/$1/; }
} elsif ($settings{'pick'}) {
my $min = $settings{'pick_min'};
my $max = $settings{'pick_max'};
$max = 100 if $max > 100;
my $count = $max;
if ($settings{'random'}) {
$count = int rand ($max + 1 - $min) + $min;
}
my @choices;
$settings{'choices'} = \@choices;
while ($count-- > 0) {
my $list = $self->make_list($context, $extracted, \%settings, %opts);
last if not @$list;
$max = @$list if $settings{'unique'} and $max > @$list;
$min = $max if $min > $max;
my $choice = $self->select_weighted_item_from_list($list);
push @choices, $choice;
}
# strip outer quotes
foreach my $choice (@choices) {
if (not $choice =~ s/^"(.*)"$/$1/) { $choice =~ s/^'(.*)'$/$1/; }
}
if ($settings{'sort+'}) {
@choices = sort { $a cmp $b } @choices;
}
if ($settings{'sort-'}) {
@choices = sort { $b cmp $a } @choices;
}
return @choices if wantarray;
if (exists $settings{'join'}) {
my $sep = $settings{'join'};
$sep = '' if not defined $sep;
$item = join $sep, @choices;
} elsif ($settings{'enumerate'} or $settings{'comma'}) {
$item = join ', ', @choices;
$item =~ s/(.*), /$1 and / if $settings{'enumerate'};
} else {
$item = $opts{nested} ? join('|', @choices) : "@choices";
}
} else {
my $list = $self->make_list($context, $extracted, \%settings, %opts);
$item = $self->select_weighted_item_from_list($list);
# strip outer quotes
if (not $item =~ s/^"(.*)"$/$1/) { $item =~ s/^'(.*)'$/$1/; }
}
return $item;
}
sub expand_factoid_selectors {
my ($self, $context, $action, %opts) = @_;
my %default_opts = (
nested => 0,
recursions => 0,
);
%opts = (%default_opts, %opts);
return '!recursion limit!' if ++$opts{recursions} > 100;
my $result = '';
while (1) {
if ($action =~ /(.*?)(?<!\\)%\s*\(.*\)/) {
$result .= $1;
} else {
last;
}
my ($extracted, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($action, '(', ')', '%', 1);
last if not length $extracted;
my $item = $self->select_item($context, $extracted, \$rest, %opts);
if ($result =~ s/\b(a|an)(\s+)$//i) {
my ($article, $trailing) = ($1, $2);
my $fixed_article = select_indefinite_article $item;
if ($article eq 'AN') {
$fixed_article = uc $fixed_article;
} elsif ($article eq 'An' or $article eq 'A') {
$fixed_article = ucfirst $fixed_article;
}
$item = $fixed_article . $trailing . $item;
}
$result .= $item;
$action = $rest;
}
$result .= $action;
return $result;
}
sub expand_factoid_vars {
my ($self, $context, $action, %opts) = @_;
my %default_opts = (
nested => 0,
recursions => 0,
);
%opts = (%default_opts, %opts);
return '!recursion limit reached!' if ++$opts{recursions} > 100;
2020-05-02 05:59:51 +02:00
my $from = length $context->{ref_from} ? $context->{ref_from} : $context->{from};
my $nick = $context->{nick};
my $root_keyword = $context->{keyword_override} ? $context->{keyword_override} : $context->{root_keyword};
$action = defined $action ? $action : $context->{action};
my $interpolate = $self->{storage}->get_data($context->{channel}, $context->{keyword}, 'interpolate');
2021-05-27 08:59:19 +02:00
return $action if defined $interpolate and $interpolate == 0;
$interpolate = $self->{pbot}->{registry}->get_value($context->{channel}, 'interpolate_factoids');
return $action if defined $interpolate and $interpolate == 0;
$action = $self->expand_factoid_selectors($context, $action, %opts);
2020-02-15 23:38:32 +01:00
my $depth = 0;
2020-02-15 23:38:32 +01:00
if ($action =~ m/^\/call --keyword-override=([^ ]+)/i) { $root_keyword = $1; }
2018-04-02 00:03:04 +02:00
my $result = '';
my $rest = $action;
2020-02-15 23:38:32 +01:00
while (1) {
last if ++$depth >= 100;
$rest =~ s/(?<!\\)\$0/$root_keyword/g;
2018-04-02 00:03:04 +02:00
2020-02-15 23:38:32 +01:00
my $matches = 0;
my $expansions = 0;
while ($rest =~ s/(.*?)(?<!\\)\$([\w|{])/$2/ms) {
$result .= $1;
my $var;
my $extract_method;
2018-04-02 00:03:04 +02:00
2020-06-12 08:34:53 +02:00
if ($rest =~ /^\{.*?\}/) {
($var, $rest) = $self->{pbot}->{interpreter}->extract_bracketed($rest, '{', '}');
if ($var =~ /:/) {
my @stuff = split /:/, $var, 2;
$var = $stuff[0];
$rest = ':' . $stuff[1] . $rest;
}
$extract_method = 'bracket';
} else {
$rest =~ s/^(\w+)//;
$var = $1;
$extract_method = 'regex';
}
if ($var =~ /^(?:_.*|[[:punct:]0-9]+|a|b|nick|channel|randomnick|arglen|args|arg\[.+\])$/i) {
# skip identifiers with leading underscores, etc
$result .= $extract_method eq 'bracket' ? '${' . $var . '}' : '$' . $var;
next;
}
$matches++;
2015-10-05 11:03:13 +02:00
# extract channel expansion modifier
2020-07-18 21:52:47 +02:00
if ($rest =~ s/^:(#[^:]+|global)//i) {
2020-02-15 23:38:32 +01:00
$from = $1;
$from = '.*' if lc $from eq 'global';
}
2020-02-15 23:38:32 +01:00
my $recurse = 0;
ALIAS:
my @factoids = $self->find_factoid($from, $var, exact_channel => 2, exact_trigger => 2);
if (not @factoids or not $factoids[0]) {
$result .= $extract_method eq 'bracket' ? '${' . $var . '}' : '$' . $var;
next;
}
my $var_chan;
($var_chan, $var) = ($factoids[0]->[0], $factoids[0]->[1]);
if ($self->{storage}->get_data($var_chan, $var, 'action') =~ m{^/call (.*)}ms) {
$var = $1;
if (++$recurse > 100) {
$self->{pbot}->{logger}->log("Factoids: variable expansion recursion limit reached\n");
$result .= $extract_method eq 'bracket' ? '${' . $var . '}' : '$' . $var;
next;
}
2020-02-15 23:38:32 +01:00
goto ALIAS;
}
2020-07-20 01:11:40 +02:00
my $copy = $rest;
my %settings = $self->parse_expansion_modifiers(\$copy);
2020-07-18 21:46:44 +02:00
if ($self->{storage}->get_data($var_chan, $var, 'type') eq 'text') {
my $change = $self->{storage}->get_data($var_chan, $var, 'action');
2020-02-15 23:38:32 +01:00
my @list = $self->{pbot}->{interpreter}->split_line($change);
my @replacements;
if (wantarray) {
@replacements = $self->select_item($context, join ('|', @list), \$rest, %opts);
2020-06-12 10:53:25 +02:00
return @replacements;
} else {
push @replacements, scalar $self->select_item($context, join ('|', @list), \$rest, %opts);
}
my $replacement = $opts{nested} ? join('|', @replacements) : "@replacements";
if (not length $replacement) {
$result =~ s/\s+$//;
} else {
$replacement = $self->expand_factoid_vars($context, $replacement, %opts);
2020-02-15 23:38:32 +01:00
}
2020-07-18 21:46:44 +02:00
if ($settings{'uc'}) { $replacement = uc $replacement; }
if ($settings{'lc'}) { $replacement = lc $replacement; }
if ($settings{'ucfirst'}) { $replacement = ucfirst $replacement; }
if ($settings{'title'}) {
$replacement = ucfirst lc $replacement;
$replacement =~ s/ (\w)/' ' . uc $1/ge;
}
if ($settings{'json'}) { $replacement = $self->escape_json($replacement); }
if ($result =~ s/\b(a|an)(\s+)$//i) {
my ($article, $trailing) = ($1, $2);
my $fixed_article = select_indefinite_article $replacement;
if ($article eq 'AN') {
$fixed_article = uc $fixed_article;
} elsif ($article eq 'An' or $article eq 'A') {
$fixed_article = ucfirst $fixed_article;
}
$replacement = $fixed_article . $trailing . $replacement;
}
$result .= $replacement;
2020-02-15 23:38:32 +01:00
$expansions++;
} else {
$result .= $extract_method eq 'bracket' ? '${' . $var . '}' : '$' . $var;
2020-02-15 23:38:32 +01:00
}
}
2020-06-12 10:53:25 +02:00
last if $matches == 0 or $expansions == 0;
if (not length $rest) {
$rest = $result;
$result = '';
}
}
$result .= $rest;
$result = $self->expand_special_vars($from, $nick, $root_keyword, $result);
# unescape certain symbols
$result =~ s/(?<!\\)\\([\$\:\|])/$1/g;
return validate_string($result, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
}
sub expand_action_arguments {
2020-02-15 23:38:32 +01:00
my ($self, $action, $input, $nick) = @_;
2020-02-15 23:38:32 +01:00
$action = validate_string($action, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
$input = validate_string($input, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
2020-02-15 23:38:32 +01:00
my %h;
if (not defined $input or $input eq '') { %h = (args => $nick); }
else { %h = (args => $input); }
my $jsonargs = to_json \%h;
2020-02-15 23:38:32 +01:00
$jsonargs =~ s/^{".*":"//;
$jsonargs =~ s/"}$//;
2020-02-15 23:38:32 +01:00
if (not defined $input or $input eq '') {
$input = "";
$action =~ s/\$args:json|\$\{args:json\}/$jsonargs/ge;
$action =~ s/\$args(?![[\w])|\$\{args(?![[\w])\}/$nick/g;
} else {
$action =~ s/\$args:json|\$\{args:json\}/$jsonargs/g;
$action =~ s/\$args(?![[\w])|\$\{args(?![[\w])\}/$input/g;
}
2020-02-15 23:38:32 +01:00
my @args = $self->{pbot}->{interpreter}->split_line($input);
$action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg;
2020-02-15 23:38:32 +01:00
my $depth = 0;
my $const_action = $action;
while ($const_action =~ m/\$arg\[([^]]+)]|\$\{arg\[([^]]+)]\}/g) {
my $arg = defined $2 ? $2 : $1;
last if ++$depth >= 100;
if ($arg eq '*') {
if (not defined $input or $input eq '') { $action =~ s/\$arg\[\*\]|\$\{arg\[\*\]\}/$nick/; }
else { $action =~ s/\$arg\[\*\]|\$\{arg\[\*\]\}/$input/; }
next;
}
2020-02-15 23:38:32 +01:00
if ($arg =~ m/([^:]*):(.*)/) {
my $arg1 = $1;
my $arg2 = $2;
2020-02-15 23:38:32 +01:00
my $arg1i = $arg1;
my $arg2i = $arg2;
2020-02-15 23:38:32 +01:00
$arg1i = 0 if $arg1i eq '';
$arg2i = $#args if $arg2i eq '';
$arg2i = $#args if $arg2i > $#args;
2020-02-15 23:38:32 +01:00
my @values = eval {
local $SIG{__WARN__} = sub { };
return @args[$arg1i .. $arg2i];
};
if ($@) { next; }
else {
my $string = join(' ', @values);
if ($string eq '') { $action =~ s/\s*\$\{arg\[$arg1:$arg2\]\}// || $action =~ s/\s*\$arg\[$arg1:$arg2\]//; }
else { $action =~ s/\$\{arg\[$arg1:$arg2\]\}/$string/ || $action =~ s/\$arg\[$arg1:$arg2\]/$string/; }
}
next;
}
my $value = eval {
local $SIG{__WARN__} = sub { };
return $args[$arg];
};
if ($@) { next; }
else {
if (not defined $value) {
if ($arg == 0) { $action =~ s/\$\{arg\[$arg\]\}/$nick/ || $action =~ s/\$arg\[$arg\]/$nick/; }
else { $action =~ s/\s*\$\{arg\[$arg\]\}// || $action =~ s/\s*\$arg\[$arg\]//; }
} else {
$action =~ s/\$arg\{\[$arg\]\}/$value/ || $action =~ s/\$arg\[$arg\]/$value/;
}
}
}
2020-02-15 23:38:32 +01:00
return $action;
}
sub escape_json {
my ($self, $text) = @_;
my $thing = {thing => $text};
my $json = to_json $thing;
$json =~ s/^{".*":"//;
$json =~ s/"}$//;
return $json;
}
sub expand_special_vars {
my ($self, $from, $nick, $root_keyword, $action) = @_;
$action =~ s/(?<!\\)\$nick:json|(?<!\\)\$\{nick:json\}/$self->escape_json($nick)/ge;
$action =~ s/(?<!\\)\$channel:json|(?<!\\)\$\{channel:json\}/$self->escape_json($from)/ge;
$action =~
s/(?<!\\)\$randomnick:json|(?<!\\)\$\{randomnick:json\}/my $random = $self->{pbot}->{nicklist}->random_nick($from); $random ? $self->escape_json($random) : $self->escape_json($nick)/ge;
$action =~ s/(?<!\\)\$0:json|(?<!\\)\$\{0:json\}/$self->escape_json($root_keyword)/ge;
$action =~ s/(?<!\\)\$nick|(?<!\\)\$\{nick\}/$nick/g;
$action =~ s/(?<!\\)\$channel|(?<!\\)\$\{channel\}/$from/g;
$action =~ s/(?<!\\)\$randomnick|(?<!\\)\$\{randomnick\}/my $random = $self->{pbot}->{nicklist}->random_nick($from); $random ? $random : $nick/ge;
$action =~ s/(?<!\\)\$0\b|(?<!\\)\$\{0\}\b/$root_keyword/g;
return validate_string($action, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
}
sub execute_code_factoid_using_vm {
2020-05-02 05:59:51 +02:00
my ($self, $context) = @_;
unless ($self->{storage}->exists($context->{channel}, $context->{keyword}, 'interpolate')
and $self->{storage}->get_data($context->{channel}, $context->{keyword}, 'interpolate') eq '0')
2020-02-15 23:38:32 +01:00
{
if ($context->{code} =~ m/(?:\$\{?nick\b|\$\{?args\b|\$\{?arg\[)/ and length $context->{arguments}) {
$context->{nickprefix_disabled} = 1;
} else {
$context->{nickprefix_disabled} = 0;
}
$context->{code} = $self->expand_factoid_vars($context, $context->{code});
if ($self->{storage}->get_data($context->{channel}, $context->{keyword}, 'allow_empty_args')) {
2020-05-02 05:59:51 +02:00
$context->{code} = $self->expand_action_arguments($context->{code}, $context->{arguments}, '');
2020-02-15 23:38:32 +01:00
} else {
2020-05-02 05:59:51 +02:00
$context->{code} = $self->expand_action_arguments($context->{code}, $context->{arguments}, $context->{nick});
2020-02-15 23:38:32 +01:00
}
} else {
# otherwise allow nick overriding
$context->{nickprefix_disabled} = 0;
}
# set up `compiler` module arguments
my %args = (
nick => $context->{nick},
channel => $context->{from},
lang => $context->{lang},
code => $context->{code},
arguments => $context->{arguments},
factoid => "$context->{channel}:$context->{keyword}",
2020-02-15 23:38:32 +01:00
);
# the vm can persist filesystem data to external storage identified by a key.
# if the `persist-key` factoid metadata is set, then use this key.
my $persist_key = $self->{storage}->get_data($context->{channel}, $context->{keyword}, 'persist-key');
if (defined $persist_key) {
$args{'persist-key'} = $persist_key;
2020-02-15 23:38:32 +01:00
}
# encode args to utf8 json string
my $json = encode_json \%args;
# update context details
$context->{special} = 'code-factoid'; # ensure handle_result(), etc, process this as a code-factoid
$context->{root_channel} = $context->{channel}; # override root channel to current channel
$context->{keyword} = 'compiler'; # code-factoid uses `compiler` command to invoke vm
$context->{arguments} = $json; # set arguments to json string as `compiler` wants
$context->{args_utf8} = 1; # arguments are utf8 encoded by encode_json
# launch the `compiler` module
2020-05-02 05:59:51 +02:00
$self->{pbot}->{modules}->execute_module($context);
# return empty string since the module process reader will
# pass the output along to the result handler
return '';
}
sub execute_code_factoid {
2020-02-15 23:38:32 +01:00
my ($self, @args) = @_;
# this sub used to contain an if-clause that selected
# an alternative method of executing code factoids.
# now it only uses the vm. maybe one day...
2020-02-15 23:38:32 +01:00
return $self->execute_code_factoid_using_vm(@args);
}
2021-07-21 07:44:51 +02:00
# main entry point for PBot::Core::Interpreter to interpret a factoid command
sub interpreter {
2020-05-02 05:59:51 +02:00
my ($self, $context) = @_;
2020-02-15 23:38:32 +01:00
my $pbot = $self->{pbot};
if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) {
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$self->{pbot}->{logger}->log("Factoids::interpreter\n");
2020-05-02 05:59:51 +02:00
$self->{pbot}->{logger}->log(Dumper $context);
}
2020-05-02 05:59:51 +02:00
return undef if not length $context->{keyword} or $context->{interpret_depth} > $self->{pbot}->{registry}->get_value('interpreter', 'max_recursion');
2020-05-02 05:59:51 +02:00
$context->{from} = lc $context->{from};
2019-06-26 18:34:19 +02:00
2020-05-02 05:59:51 +02:00
my $strictnamespace = $self->{pbot}->{registry}->get_value($context->{from}, 'strictnamespace');
2018-05-12 11:52:52 +02:00
2020-02-15 23:38:32 +01:00
if (not defined $strictnamespace) { $strictnamespace = $self->{pbot}->{registry}->get_value('general', 'strictnamespace'); }
2018-05-12 11:52:52 +02:00
2020-02-15 23:38:32 +01:00
# search for factoid against global channel and current channel (from unless ref_from is defined)
2020-05-02 05:59:51 +02:00
my $original_keyword = $context->{keyword};
2020-02-15 23:38:32 +01:00
my ($channel, $keyword) =
2020-05-02 05:59:51 +02:00
$self->find_factoid($context->{ref_from} ? $context->{ref_from} : $context->{from}, $context->{keyword}, arguments => $context->{arguments}, exact_channel => 1);
2020-05-02 05:59:51 +02:00
if (not $context->{ref_from} or $context->{ref_from} eq '.*' or $context->{ref_from} eq $context->{from}) { $context->{ref_from} = ""; }
2020-05-02 05:59:51 +02:00
if (defined $channel and not $channel eq '.*' and not $channel eq lc $context->{from}) { $context->{ref_from} = $channel; }
2020-05-02 05:59:51 +02:00
$context->{arguments} = "" if not defined $context->{arguments};
# factoid > nick redirection
my $nick_regex = $self->{pbot}->{registry}->get_value('regex', 'nickname');
if ($context->{arguments} =~ s/> ($nick_regex)$//) {
my $rcpt = $1;
if ($self->{pbot}->{nicklist}->is_present($context->{from}, $rcpt)) {
$context->{nickprefix} = $rcpt;
$context->{nickprefix_forced} = 1;
} else {
$context->{arguments} .= "> $rcpt";
}
}
2020-02-15 23:38:32 +01:00
# if no match found, attempt to call factoid from another channel if it exists there
if (not defined $keyword) {
2020-05-02 05:59:51 +02:00
my $string = "$original_keyword $context->{arguments}";
2020-02-15 23:38:32 +01:00
2020-02-26 11:32:22 +01:00
my @chanlist = ();
2020-02-15 23:38:32 +01:00
my ($fwd_chan, $fwd_trig);
2021-05-27 06:42:01 +02:00
unless ($strictnamespace) {
# build list of which channels contain the keyword, keeping track of the last one and count
foreach my $factoid ($self->{storage}->get_all("index2 = $original_keyword", 'index1', 'type')) {
2021-05-27 06:42:01 +02:00
next if $factoid->{type} ne 'text' and $factoid->{type} ne 'module';
push @chanlist, $self->{storage}->get_data($factoid->{index1}, '_name');
2021-05-27 06:42:01 +02:00
$fwd_chan = $factoid->{index1};
$fwd_trig = $original_keyword;
}
2020-02-15 23:38:32 +01:00
}
2020-05-02 05:59:51 +02:00
my $ref_from = $context->{ref_from} ? "[$context->{ref_from}] " : "";
2020-02-15 23:38:32 +01:00
# if multiple channels have this keyword, then ask user to disambiguate
2020-02-26 11:32:22 +01:00
if (@chanlist> 1) {
2021-07-24 03:26:45 +02:00
return undef if $context->{embedded};
return $ref_from . "Factoid `$original_keyword` exists in " . join(', ', @chanlist) . "; use `fact <channel> $original_keyword` to choose one.";
2020-02-15 23:38:32 +01:00
}
# if there's just one other channel that has this keyword, trigger that instance
2020-02-26 11:32:22 +01:00
elsif (@chanlist == 1) {
2020-02-15 23:38:32 +01:00
$pbot->{logger}->log("Found '$original_keyword' as '$fwd_trig' in [$fwd_chan]\n");
2020-05-02 05:59:51 +02:00
$context->{keyword} = $fwd_trig;
$context->{interpret_depth}++;
$context->{ref_from} = $fwd_chan;
return $self->interpreter($context);
2020-02-15 23:38:32 +01:00
}
# otherwise keyword hasn't been found, display similiar matches for all channels
else {
2020-05-02 05:59:51 +02:00
my $namespace = $strictnamespace ? $context->{from} : '.*';
2020-02-15 23:38:32 +01:00
$namespace = '.*' if $namespace !~ /^#/;
2020-01-17 06:51:03 +01:00
2020-02-15 23:38:32 +01:00
my $namespace_regex = $namespace;
if ($strictnamespace) { $namespace_regex = "(?:" . (quotemeta $namespace) . '|\\.\\*)'; }
$context->{arguments} = quotemeta($original_keyword) . " -channel $namespace_regex";
2021-07-22 14:36:46 +02:00
my $matches = $self->{pbot}->{commands}->{modules}->{Factoids}->cmd_factfind($context);
2020-02-15 23:38:32 +01:00
# found factfind matches
if ($matches !~ m/^No factoids/) {
2021-07-24 03:26:45 +02:00
return undef if $context->{embedded};
2020-02-15 23:38:32 +01:00
return "No such factoid '$original_keyword'; $matches";
}
2020-02-15 23:38:32 +01:00
# otherwise find levenshtein closest matches
$matches = $self->{storage}->levenshtein_matches($namespace, lc $original_keyword, 0.50, $strictnamespace);
# if a non-nick argument was supplied, e.g., a sentence using the bot's nick, /msg the error to the caller
2020-05-02 05:59:51 +02:00
if (length $context->{arguments} and not $self->{pbot}->{nicklist}->is_present($context->{from}, $context->{arguments})) {
$context->{send_msg_to_caller} = 1;
}
# /msg the caller if nothing similiar was found
2020-05-02 05:59:51 +02:00
$context->{send_msg_to_caller} = 1 if $matches eq 'none';
2021-07-24 03:26:45 +02:00
$context->{send_msg_to_caller} = 1 if $context->{embedded};
my $msg_caller = '';
2020-05-02 05:59:51 +02:00
$msg_caller = "/msg $context->{nick} " if $context->{send_msg_to_caller};
2020-02-15 23:38:32 +01:00
2020-05-02 05:59:51 +02:00
my $ref_from = $context->{ref_from} ? "[$context->{ref_from}] " : "";
if ($matches eq 'none') {
return $msg_caller . $ref_from . "No such factoid '$original_keyword'; no similar matches.";
} else {
return $msg_caller . $ref_from . "No such factoid '$original_keyword'; did you mean $matches?";
}
2020-02-15 23:38:32 +01:00
}
2019-05-13 09:24:08 +02:00
}
2020-02-15 23:38:32 +01:00
my $channel_name = $self->{storage}->get_data($channel, '_name');
my $trigger_name = $self->{storage}->get_data($channel, $keyword, '_name');
2020-02-15 23:38:32 +01:00
$channel_name = 'global' if $channel_name eq '.*';
$trigger_name = "\"$trigger_name\"" if $trigger_name =~ / /;
2020-05-02 05:59:51 +02:00
$context->{keyword} = $keyword;
$context->{trigger} = $keyword;
$context->{channel} = $channel;
$context->{original_keyword} = $original_keyword;
$context->{channel_name} = $channel_name;
$context->{trigger_name} = $trigger_name;
2020-02-15 23:38:32 +01:00
2021-07-24 03:26:45 +02:00
return undef if $context->{embedded} and $self->{storage}->get_data($channel, $keyword, 'noembed');
2020-02-15 23:38:32 +01:00
if ($self->{storage}->get_data($channel, $keyword, 'locked_to_channel')) {
2020-05-02 05:59:51 +02:00
if ($context->{ref_from} ne "") { # called from another channel
return "$trigger_name may be invoked only in $context->{ref_from}.";
}
}
2020-02-15 23:38:32 +01:00
if ($context->{interpret_depth} <= 1 and $self->{storage}->get_data($channel, $keyword, 'last_referenced_in') eq $context->{from}) {
my $ratelimit = $self->{pbot}->{registry}->get_value($context->{from}, 'ratelimit_override');
$ratelimit = $self->{storage}->get_data($channel, $keyword, 'rate_limit') if not defined $ratelimit;
if (gettimeofday - $self->{storage}->get_data($channel, $keyword, 'last_referenced_on') < $ratelimit) {
my $ref_from = $context->{ref_from} ? "[$context->{ref_from}] " : "";
return
"/msg $context->{nick} $ref_from'$trigger_name' is rate-limited; try again in "
. duration($ratelimit - int(gettimeofday - $self->{storage}->get_data($channel, $keyword, 'last_referenced_on'))) . "."
unless $self->{pbot}->{users}->loggedin_admin($channel, "$context->{nick}!$context->{user}\@$context->{host}");
2020-02-15 23:38:32 +01:00
}
}
my $ref_count = $self->{storage}->get_data($channel, $keyword, 'ref_count');
2020-02-26 11:32:22 +01:00
my $update_data = {
ref_count => ++$ref_count,
2020-05-02 05:59:51 +02:00
ref_user => "$context->{nick}!$context->{user}\@$context->{host}",
2020-02-26 11:32:22 +01:00
last_referenced_on => scalar gettimeofday,
2020-05-02 05:59:51 +02:00
last_referenced_in => $context->{from} || 'stdin',
2020-02-26 11:32:22 +01:00
};
$self->{storage}->add($channel, $keyword, $update_data, 1);
2017-08-24 04:25:43 +02:00
2020-02-15 23:38:32 +01:00
my $action;
if ($self->{storage}->exists($channel, $keyword, 'usage') and not length $context->{arguments} and $self->{storage}->get_data($channel, $keyword, 'requires_arguments')) {
2020-05-02 05:59:51 +02:00
$context->{alldone} = 1;
my $usage = $self->{storage}->get_data($channel, $keyword, 'usage');
$usage =~ s/(?<!\\)\$0|(?<!\\)\$\{0\}/$trigger_name/g;
2020-02-15 23:38:32 +01:00
return $usage;
}
2020-02-15 23:38:32 +01:00
if (length $context->{arguments} and $self->{storage}->exists($channel, $keyword, 'action_with_args')) {
$action = $self->{storage}->get_data($channel, $keyword, 'action_with_args');
} else {
$action = $self->{storage}->get_data($channel, $keyword, 'action');
2020-02-15 23:38:32 +01:00
}
2020-02-15 23:38:32 +01:00
if ($action =~ m{^/code\s+([^\s]+)\s+(.+)$}msi) {
my ($lang, $code) = ($1, $2);
if ($self->{storage}->exists($channel, $keyword, 'usage') and not length $context->{arguments}) {
2020-05-02 05:59:51 +02:00
$context->{alldone} = 1;
my $usage = $self->{storage}->get_data($channel, $keyword, 'usage');
$usage =~ s/(?<!\\)\$0|(?<!\\)\$\{0\}/$trigger_name/g;
2020-02-15 23:38:32 +01:00
return $usage;
}
2020-02-15 23:38:32 +01:00
2020-05-02 05:59:51 +02:00
$context->{lang} = $lang;
$context->{code} = $code;
$self->execute_code_factoid($context);
2020-02-15 23:38:32 +01:00
return "";
}
2020-02-15 23:38:32 +01:00
if ($self->{storage}->get_data($channel, $keyword, 'background-process')) {
my $timeout = $self->{storage}->get_data($channel, $keyword, 'process-timeout') // $self->{pbot}->{registry}->get_value('processmanager', 'default_timeout');
$self->{pbot}->{process_manager}->execute_process(
2020-05-02 05:59:51 +02:00
$context,
sub { $context->{result} = $self->handle_action($context, $action); },
$timeout
);
return "";
} else {
2020-05-02 05:59:51 +02:00
return $self->handle_action($context, $action);
}
2020-02-15 23:38:32 +01:00
}
sub handle_action {
2020-05-02 05:59:51 +02:00
my ($self, $context, $action) = @_;
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("Factoids::handle_action [$action]\n");
2020-05-02 05:59:51 +02:00
$self->{pbot}->{logger}->log(Dumper $context);
}
2020-02-15 23:38:32 +01:00
return "" if not length $action;
2020-05-02 05:59:51 +02:00
my ($channel, $keyword) = ($context->{channel}, $context->{trigger});
my ($channel_name, $trigger_name) = ($context->{channel_name}, $context->{trigger_name});
my $ref_from = '';
unless ($context->{pipe} or $context->{subcmd}) {
$ref_from = $context->{ref_from} ? "[$context->{ref_from}] " : '';
}
2020-02-15 23:38:32 +01:00
unless ($self->{storage}->exists($channel, $keyword, 'interpolate') and $self->{storage}->get_data($channel, $keyword, 'interpolate') eq '0') {
my ($root_channel, $root_keyword) =
$self->find_factoid($context->{ref_from} ? $context->{ref_from} : $context->{from}, $context->{root_keyword}, arguments => $context->{arguments}, exact_channel => 1);
if (not defined $root_channel or not defined $root_keyword) {
$root_channel = $channel;
$root_keyword = $keyword;
}
if (not length $context->{keyword_override} and length $self->{storage}->get_data($root_channel, $root_keyword, 'keyword_override')) {
$context->{keyword_override} = $self->{storage}->get_data($root_channel, $root_keyword, 'keyword_override');
}
$action = $self->expand_factoid_vars($context, $action);
}
2020-05-02 05:59:51 +02:00
if (length $context->{arguments}) {
2020-02-15 23:38:32 +01:00
if ($action =~ m/\$\{?args/ or $action =~ m/\$\{?arg\[/) {
unless (defined $self->{storage}->get_data($channel, $keyword, 'interpolate') and $self->{storage}->get_data($channel, $keyword, 'interpolate') eq '0') {
2020-05-02 05:59:51 +02:00
$action = $self->expand_action_arguments($action, $context->{arguments}, $context->{nick});
2020-02-15 23:38:32 +01:00
}
2020-05-02 05:59:51 +02:00
$context->{arguments} = "";
$context->{original_arguments} = "";
2020-02-15 23:38:32 +01:00
} else {
if ($self->{storage}->get_data($channel, $keyword, 'type') eq 'text') {
2020-05-02 05:59:51 +02:00
my $target = $self->{pbot}->{nicklist}->is_present_similar($context->{from}, $context->{arguments});
2020-02-15 23:38:32 +01:00
if ($target and $action !~ /\$\{?(?:nick|args)\b/) {
$context->{nickprefix} = $target unless $context->{nickprefix_forced};
$context->{nickprefix_disabled} = 0;
2020-02-15 23:38:32 +01:00
}
}
}
} else {
# no arguments supplied, replace $args with $nick/$tonick, etc
if ($self->{storage}->exists($channel, $keyword, 'usage')) {
$action = "/say " . $self->{storage}->get_data($channel, $keyword, 'usage');
$action =~ s/(?<!\\)\$0|(?<!\\)\$\{0\}/$trigger_name/g;
2020-05-02 05:59:51 +02:00
$context->{alldone} = 1;
2019-05-13 09:24:08 +02:00
} else {
if ($self->{storage}->get_data($channel, $keyword, 'allow_empty_args')) {
$action = $self->expand_action_arguments($action, undef, '');
} else {
$action = $self->expand_action_arguments($action, undef, $context->{nick});
}
2019-05-13 09:24:08 +02:00
}
$context->{nickprefix_disabled} = 0;
2019-05-13 09:24:08 +02:00
}
2020-02-15 23:38:32 +01:00
# Check if it's an alias
if ($action =~ /^\/call\s+(.*)$/msi) {
my $command = $1;
$command =~ s/\n$//;
unless ($self->{storage}->get_data($channel, $keyword, 'require_explicit_args')) {
2020-05-02 05:59:51 +02:00
my $args = $context->{arguments};
$command .= " $args" if length $args and not $context->{special} eq 'code-factoid';
$context->{arguments} = '';
2020-02-15 23:38:32 +01:00
}
unless ($self->{storage}->get_data($channel, $keyword, 'no_keyword_override')) {
2020-05-02 05:59:51 +02:00
if ($command =~ s/\s*--keyword-override=([^ ]+)\s*//) { $context->{keyword_override} = $1; }
2020-02-15 23:38:32 +01:00
}
2020-05-02 05:59:51 +02:00
$context->{command} = $command;
$context->{aliased} = 1;
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}
2020-05-02 05:59:51 +02:00
->log("[" . (defined $context->{from} ? $context->{from} : "stdin") . "] ($context->{nick}!$context->{user}\@$context->{host}) $trigger_name aliased to: $command\n");
2010-04-02 19:33:18 +02:00
if (defined $self->{storage}->get_data($channel, $keyword, 'cap-override')) {
if ($self->{storage}->get_data($channel, $keyword, 'locked')) {
$self->{pbot}->{logger}->log("Capability override set to " . $self->{storage}->get_data($channel, $keyword, 'cap-override') . "\n");
$context->{'cap-override'} = $self->{storage}->get_data($channel, $keyword, 'cap-override');
2020-02-15 23:38:32 +01:00
} else {
$self->{pbot}->{logger}->log("Ignoring cap-override of " . $self->{storage}->get_data($channel, $keyword, 'cap-override') . " on unlocked factoid\n");
2020-02-15 23:38:32 +01:00
}
}
2020-05-02 05:59:51 +02:00
return $self->{pbot}->{interpreter}->interpret($context);
}
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}
2020-05-02 05:59:51 +02:00
->log("(" . (defined $context->{from} ? $context->{from} : "(undef)") . "): $context->{nick}!$context->{user}\@$context->{host}: $trigger_name: action: \"$action\"\n");
2020-02-15 23:38:32 +01:00
my $enabled = $self->{storage}->get_data($channel, $keyword, 'enabled');
2020-06-07 09:12:09 +02:00
if (defined $enabled and $enabled == 0) {
2020-02-15 23:38:32 +01:00
$self->{pbot}->{logger}->log("$trigger_name disabled.\n");
2020-05-02 05:59:51 +02:00
return "/msg $context->{nick} ${ref_from}$trigger_name is currently disabled.";
}
unless ($self->{storage}->exists($channel, $keyword, 'interpolate') and $self->{storage}->get_data($channel, $keyword, 'interpolate') eq '0') {
2020-02-15 23:38:32 +01:00
my ($root_channel, $root_keyword) =
2020-05-02 05:59:51 +02:00
$self->find_factoid($context->{ref_from} ? $context->{ref_from} : $context->{from}, $context->{root_keyword}, arguments => $context->{arguments}, exact_channel => 1);
2020-02-15 23:38:32 +01:00
if (not defined $root_channel or not defined $root_keyword) {
$root_channel = $channel;
$root_keyword = $keyword;
}
if (not length $context->{keyword_override} and length $self->{storage}->get_data($root_channel, $root_keyword, 'keyword_override')) {
$context->{keyword_override} = $self->{storage}->get_data($root_channel, $root_keyword, 'keyword_override');
2020-02-15 23:38:32 +01:00
}
$action = $self->expand_factoid_vars($context, $action);
if ($self->{storage}->get_data($channel, $keyword, 'allow_empty_args')) {
$action = $self->expand_action_arguments($action, $context->{arguments}, '');
} else {
$action = $self->expand_action_arguments($action, $context->{arguments}, $context->{nick});
}
}
2020-05-02 05:59:51 +02:00
return $action if $context->{special} eq 'code-factoid';
if ($self->{storage}->get_data($channel, $keyword, 'type') eq 'module') {
my $preserve_whitespace = $self->{storage}->get_data($channel, $keyword, 'preserve_whitespace');
2020-02-15 23:38:32 +01:00
$preserve_whitespace = 0 if not defined $preserve_whitespace;
2010-04-02 19:33:18 +02:00
2020-05-02 05:59:51 +02:00
$context->{preserve_whitespace} = $preserve_whitespace;
$context->{root_keyword} = $keyword unless defined $context->{root_keyword};
$context->{root_channel} = $channel;
2020-05-02 05:59:51 +02:00
my $result = $self->{pbot}->{modules}->execute_module($context);
if (length $result) {
return $ref_from . $result;
} else {
return "";
}
} elsif ($self->{storage}->get_data($channel, $keyword, 'type') eq 'text') {
2020-02-15 23:38:32 +01:00
# Don't allow user-custom /msg factoids, unless factoid triggered by admin
if ($action =~ m/^\/msg/i) {
2020-06-21 06:37:21 +02:00
if (not $self->{pbot}->{users}->loggedin_admin($context->{from}, $context->{hostmask})) {
$self->{pbot}->{logger}->log("[ABUSE] Bad factoid (starts with /msg): $action\n");
return "You must be an admin to use /msg.";
2020-02-15 23:38:32 +01:00
}
}
2010-06-30 05:48:13 +02:00
2020-02-15 23:38:32 +01:00
if ($ref_from) {
2020-06-21 06:37:21 +02:00
if ( $action =~ s/^\/say\s+/$ref_from/i
|| $action =~ s/^\/me\s+(.*)/\/me $1 $ref_from/i
|| $action =~ s/^\/msg\s+([^ ]+)/\/msg $1 $ref_from/i
) {
return $action;
} else {
return $ref_from . "$trigger_name is $action";
}
2020-02-15 23:38:32 +01:00
} else {
if ($action =~ m/^\/(?:say|me|msg)/i) { return $action; }
else { return "/say $trigger_name is $action"; }
}
} elsif ($self->{storage}->get_data($channel, $keyword, 'type') eq 'regex') {
2020-02-15 23:38:32 +01:00
my $result = eval {
2020-05-02 05:59:51 +02:00
my $string = "$context->{original_keyword}" . (defined $context->{arguments} ? " $context->{arguments}" : "");
2020-02-15 23:38:32 +01:00
my $cmd;
if ($string =~ m/$keyword/i) {
$self->{pbot}->{logger}->log("[$string] matches [$keyword] - calling [" . $action . "$']\n");
$cmd = $action . $';
my ($a, $b, $c, $d, $e, $f, $g, $h, $i, $before, $after) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $`, $');
$cmd =~ s/\$1/$a/g;
$cmd =~ s/\$2/$b/g;
$cmd =~ s/\$3/$c/g;
$cmd =~ s/\$4/$d/g;
$cmd =~ s/\$5/$e/g;
$cmd =~ s/\$6/$f/g;
$cmd =~ s/\$7/$g/g;
$cmd =~ s/\$8/$h/g;
$cmd =~ s/\$9/$i/g;
$cmd =~ s/\$`/$before/g;
$cmd =~ s/\$'/$after/g;
$cmd =~ s/^\s+//;
$cmd =~ s/\s+$//;
} else {
$cmd = $action;
}
2020-05-02 05:59:51 +02:00
$context->{command} = $cmd;
return $self->{pbot}->{interpreter}->interpret($context);
2020-02-15 23:38:32 +01:00
};
if ($@) {
$self->{pbot}->{logger}->log("Regex fail: $@\n");
return "";
}
2010-04-02 19:33:18 +02:00
2020-02-15 23:38:32 +01:00
if (length $result) { return $ref_from . $result; }
else { return ""; }
} else {
2020-05-02 05:59:51 +02:00
$self->{pbot}->{logger}->log("($context->{from}): $context->{nick}!$context->{user}\@$context->{host}): Unknown command type for '$trigger_name'\n");
2020-02-15 23:38:32 +01:00
return "/me blinks." . " $ref_from";
}
}
1;