3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-05 11:39:34 +01:00
pbot/PBot/Interpreter.pm
2021-06-06 19:12:14 -07:00

1076 lines
39 KiB
Perl

# File: Interpreter.pm
# Author: pragma_
#
# Purpose:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::Interpreter;
use parent 'PBot::Class', 'PBot::Registerable';
use warnings; use strict;
use feature 'unicode_strings';
use utf8;
use Time::HiRes qw/gettimeofday/;
use Time::Duration;
use PBot::Utils::ValidateString;
sub initialize {
my ($self, %conf) = @_;
$self->PBot::Registerable::initialize(%conf);
$self->{pbot}->{registry}->add_default('text', 'general', 'compile_blocks', $conf{compile_blocks} // 1);
$self->{pbot}->{registry}->add_default('array', 'general', 'compile_blocks_channels', $conf{compile_blocks_channels} // '.*');
$self->{pbot}->{registry}->add_default('array', 'general', 'compile_blocks_ignore_channels', $conf{compile_blocks_ignore_channels} // 'none');
$self->{pbot}->{registry}->add_default('text', 'interpreter', 'max_recursion', 10);
}
sub process_line {
my ($self, $from, $nick, $user, $host, $text) = @_;
$from = lc $from if defined $from;
my $context = {from => $from, nick => $nick, user => $user, host => $host, hostmask => "$nick!$user\@$host", text => $text};
my $pbot = $self->{pbot};
my $message_account = $pbot->{messagehistory}->get_message_account($nick, $user, $host);
$pbot->{messagehistory}->add_message($message_account, $context->{hostmask}, $from, $text, $pbot->{messagehistory}->{MSG_CHAT});
$context->{message_account} = $message_account;
my $flood_threshold = $pbot->{registry}->get_value($from, 'chat_flood_threshold');
my $flood_time_threshold = $pbot->{registry}->get_value($from, 'chat_flood_time_threshold');
$flood_threshold = $pbot->{registry}->get_value('antiflood', 'chat_flood_threshold') if not defined $flood_threshold;
$flood_time_threshold = $pbot->{registry}->get_value('antiflood', 'chat_flood_time_threshold') if not defined $flood_time_threshold;
=cut
if (defined $from and $from =~ m/^#/) {
my $chanmodes = $self->{pbot}->{channels}->get_meta($from, 'MODE');
if (defined $chanmodes and $chanmodes =~ m/z/) {
$context->{'chan-z'} = 1;
if ($self->{pbot}->{banlist}->{quietlist}->exists($from, '$~a')) {
my $nickserv = $self->{pbot}->{messagehistory}->{database}->get_current_nickserv_account($message_account);
if (not defined $nickserv or not length $nickserv) { $context->{unidentified} = 1; }
}
$context->{banned} = 1 if $self->{pbot}->{banlist}->is_banned($nick, $user, $host, $from);
}
}
=cut
$pbot->{antiflood}->check_flood(
$from, $nick, $user, $host, $text,
$flood_threshold, $flood_time_threshold,
$pbot->{messagehistory}->{MSG_CHAT}, $context
) if defined $from;
=cut
if ($context->{banned} or $context->{unidentified}) {
$self->{pbot}->{logger}->log("Disregarding banned/unidentified user message (channel $from is +z).\n");
return 1;
}
=cut
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
# get channel-specific trigger if available
my $bot_trigger = $pbot->{registry}->get_value($from, 'trigger');
# otherwise get general trigger
if (not defined $bot_trigger) { $bot_trigger = $pbot->{registry}->get_value('general', 'trigger'); }
my $nick_regex = qr/[^%!,:\(\)\+\*\/ ]+/;
my $nick_override;
my $processed = 0;
my $preserve_whitespace = 0;
$text =~ s/^\s+//;
$text =~ s/\s+$//;
$text = validate_string($text, 0);
my $cmd_text = $text;
$cmd_text =~ s/^\/me\s+//;
# check for bot command invocation
my @commands;
my $command;
my $embedded = 0;
if ($cmd_text =~ m/^\s*($nick_regex)[,:]?\s+$bot_trigger\{\s*(.+?)\s*\}\s*$/) { goto CHECK_EMBEDDED_CMD; }
elsif ($cmd_text =~ m/^\s*$bot_trigger\{\s*(.+?)\s*\}\s*$/) { goto CHECK_EMBEDDED_CMD; }
elsif ($cmd_text =~ m/^\s*($nick_regex)[,:]\s+$bot_trigger\s*(.+)$/) {
my $possible_nick_override = $1;
$command = $2;
my $similar = $self->{pbot}->{nicklist}->is_present_similar($from, $possible_nick_override);
if ($similar) { $nick_override = $similar; }
else {
$self->{pbot}->{logger}->log("No similar nick for $possible_nick_override\n");
return 0;
}
} elsif ($cmd_text =~ m/^$bot_trigger\s*(.+)$/) {
$command = $1;
} elsif ($cmd_text =~ m/^.?\s*$botnick\s*[[:punct:]]?\s+(.+)$/i) {
$command = $1;
} elsif ($cmd_text =~ m/^(.+?),?\s*$botnick[?!.]*$/i) {
$command = $1;
}
# check for embedded commands
CHECK_EMBEDDED_CMD:
if (not defined $command or $command =~ m/^\{.*\}/) {
if ($cmd_text =~ s/^\s*($nick_regex)[,:]\s+//) {
my $possible_nick_override = $1;
my $similar = $self->{pbot}->{nicklist}->is_present_similar($from, $possible_nick_override);
if ($similar) { $nick_override = $similar; }
}
for (my $count = 0; $count < 3; $count++) {
my ($extracted, $rest) = $self->extract_bracketed($cmd_text, '{', '}', $bot_trigger);
last if not length $extracted;
$cmd_text = $rest;
$extracted =~ s/^\s+|\s+$//g;
push @commands, $extracted;
$embedded = 1;
}
} else {
push @commands, $command;
}
foreach $command (@commands) {
# check if user is ignored (and command isn't `login`)
if ($command !~ /^login / && defined $from && $pbot->{ignorelist}->is_ignored($from, "$nick!$user\@$host")) {
$self->{pbot}->{logger}->log("Disregarding command from ignored user $nick!$user\@$host in $from.\n");
return 1;
}
$context->{text} = $text;
$context->{command} = $command;
if ($nick_override) {
$context->{nickoverride} = $nick_override;
$context->{force_nickoverride} = 1;
}
$context->{referenced} = $embedded;
$context->{interpret_depth} = 0;
$context->{preserve_whitespace} = $preserve_whitespace;
$context->{result} = $self->interpret($context);
$self->handle_result($context);
$processed++;
}
return $processed;
}
sub interpret {
my ($self, $context) = @_;
my ($keyword, $arguments) = ('', '');
my $text;
my $pbot = $self->{pbot};
if (not exists $context->{hostmask}) {
$context->{hostmask} = "$context->{nick}!$context->{user}\@$context->{host}";
}
$context->{interpret_depth}++;
$pbot->{logger}->log("=== [$context->{interpret_depth}] Got command: ("
. (defined $context->{from} ? $context->{from} : "undef")
. ") $context->{hostmask}: $context->{command}\n");
$context->{special} = "" unless exists $self->{special};
if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) {
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$self->{pbot}->{logger}->log("Interpreter::interpret\n");
$self->{pbot}->{logger}->log(Dumper $context);
}
return "Too many levels of recursion, aborted." if ($context->{interpret_depth} > $self->{pbot}->{registry}->get_value('interpreter', 'max_recursion'));
if (not defined $context->{nick} || not defined $context->{user} || not defined $context->{host} || not defined $context->{command}) {
$pbot->{logger}->log("Error 1, bad parameters to interpret_command\n");
return undef;
}
# check for splitted commands
if ($context->{command} =~ m/^(.*?)\s*(?<!\\);;;\s*(.*)/ms) {
$context->{command} = $1;
$context->{command_split} = $2;
}
my $cmdlist = $self->make_args($context->{command});
$context->{commands} = [] unless exists $context->{commands};
push @{$context->{commands}}, $context->{command};
if ($self->arglist_size($cmdlist) >= 4 and lc $cmdlist->[0] eq 'tell' and (lc $cmdlist->[2] eq 'about' or lc $cmdlist->[2] eq 'the')) {
# tell nick about/the cmd [args]
$context->{nickoverride} = $cmdlist->[1];
($keyword, $arguments) = $self->split_args($cmdlist, 2, 3, 1);
$arguments = '' if not defined $arguments;
my $similar = $self->{pbot}->{nicklist}->is_present_similar($context->{from}, $context->{nickoverride});
if ($similar) {
$context->{nickoverride} = $similar;
$context->{force_nickoverride} = 1;
} else {
delete $context->{nickoverride};
delete $context->{force_nickoverride};
}
} elsif ($self->arglist_size($cmdlist) >= 3 and lc $cmdlist->[0] eq 'give') {
# give nick cmd [args]
$context->{nickoverride} = $cmdlist->[1];
($keyword, $arguments) = $self->split_args($cmdlist, 2, 2, 1);
$arguments = '' if not defined $arguments;
my $similar = $self->{pbot}->{nicklist}->is_present_similar($context->{from}, $context->{nickoverride});
if ($similar) {
$context->{nickoverride} = $similar;
$context->{force_nickoverride} = 1;
} else {
delete $context->{nickoverride};
delete $context->{force_nickoverride};
}
} else {
# normal command
($keyword, $arguments) = $self->split_args($cmdlist, 2, 0, 1);
$arguments = '' if not defined $arguments;
}
# FIXME: make this a registry item
if (length $keyword > 128) {
$keyword = substr($keyword, 0, 128);
$self->{pbot}->{logger}->log("Truncating keyword to 128 chars: $keyword\n");
}
# parse out a substituted command
if (defined $arguments && $arguments =~ m/(?<!\\)&\s*\{/) {
my ($command) = $self->extract_bracketed($arguments, '{', '}', '&', 1);
if (length $command) {
$arguments =~ s/&\s*\{\Q$command\E\}/&{subcmd}/;
push @{$context->{subcmd}}, "$keyword $arguments";
$command =~ s/^\s+|\s+$//g;
$context->{command} = $command;
$context->{commands} = [];
push @{$context->{commands}}, $command;
$context->{result} = $self->interpret($context);
return $context->{result};
}
}
# parse out a pipe
if (defined $arguments && $arguments =~ m/(?<!\\)\|\s*\{\s*[^}]+\}\s*$/) {
my ($pipe, $rest) = $self->extract_bracketed($arguments, '{', '}', '|', 1);
$arguments =~ s/\s*(?<!\\)\|\s*{(\Q$pipe\E)}.*$//s;
$pipe =~ s/^\s+|\s+$//g;
if (exists $context->{pipe}) {
$context->{pipe_rest} = "$rest | { $context->{pipe} }$context->{pipe_rest}";
} else {
$context->{pipe_rest} = $rest;
}
$context->{pipe} = $pipe;
}
if ( not $self->{pbot}->{commands}->get_meta($keyword, 'dont-replace-pronouns')
and not $self->{pbot}->{factoids}->get_meta($context->{from}, $keyword, 'dont-replace-pronouns'))
{
$context->{nickoverride} = $context->{nick} if defined $context->{nickoverride} and lc $context->{nickoverride} eq 'me';
$keyword =~ s/(\w+)([?!.]+)$/$1/;
$arguments =~ s/(?<![\w\/\-\\])i am\b/$context->{nick} is/gi if defined $arguments && $context->{interpret_depth} <= 1;
$arguments =~ s/(?<![\w\/\-\\])me\b/$context->{nick}/gi if defined $arguments && $context->{interpret_depth} <= 1;
$arguments =~ s/(?<![\w\/\-\\])my\b/$context->{nick}'s/gi if defined $arguments && $context->{interpret_depth} <= 1;
$arguments =~ s/\\my\b/my/gi if defined $arguments && $context->{interpret_depth} <= 1;
$arguments =~ s/\\me\b/me/gi if defined $arguments && $context->{interpret_depth} <= 1;
$arguments =~ s/\\i am\b/i am/gi if defined $arguments && $context->{interpret_depth} <= 1;
}
if (not $self->{pbot}->{commands}->get_meta($keyword, 'dont-protect-self') and not $self->{pbot}->{factoids}->get_meta($context->{from}, $keyword, 'dont-protect-self')) {
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
if (defined $arguments && ($arguments =~ m/^(your|him|her|its|it|them|their)(self|selves)$/i || $arguments =~ m/^$botnick$/i)) {
my $delay = rand(10) + 5;
my $message = {
nick => $context->{nick}, user => $context->{user}, host => $context->{host}, command => $context->{command}, checkflood => 1,
message => "$context->{nick}: Why would I want to do that to myself?"
};
$self->add_message_to_output_queue($context->{from}, $message, $delay);
$delay = duration($delay);
$self->{pbot}->{logger}->log("($delay delay) $message->{message}\n");
return undef;
}
}
if (not defined $keyword) {
$pbot->{logger}->log("Error 2, no keyword\n");
return undef;
}
if (not exists $context->{root_keyword}) { $context->{root_keyword} = $keyword; }
$context->{keyword} = $keyword;
$context->{original_arguments} = $arguments;
# unescape any escaped command splits
$arguments =~ s/\\;;;/;;;/g if defined $arguments;
# unescape any escaped substituted commands
$arguments =~ s/\\&\s*\{/&{/g if defined $arguments;
# unescape any escaped pipes
$arguments =~ s/\\\|\s*\{/| {/g if defined $arguments;
$arguments = validate_string($arguments);
# set arguments as a plain string
$context->{arguments} = $arguments;
delete $context->{args_utf8};
# set arguments as an array
$context->{arglist} = $self->make_args($arguments);
# execute all registered interpreters
my $result;
foreach my $func (@{$self->{handlers}}) {
$result = &{$func->{subref}}($context);
last if defined $result;
# reset any manipulated arguments
$context->{arguments} = $context->{original_arguments};
delete $context->{args_utf8};
}
return $result;
}
# extracts a bracketed substring, gracefully handling unbalanced quotes
# or brackets. opening and closing brackets may each be more than one character.
# optional prefix may be or begin with a character group.
sub extract_bracketed {
my ($self, $string, $open_bracket, $close_bracket, $optional_prefix, $allow_whitespace) = @_;
$open_bracket = '{' if not defined $open_bracket;
$close_bracket = '}' if not defined $close_bracket;
$optional_prefix = '' if not defined $optional_prefix;
$allow_whitespace = 0 if not defined $allow_whitespace;
my @prefix_group;
if ($optional_prefix =~ s/^\[(.*?)\]//) { @prefix_group = split //, $1; }
my @prefixes = split //, $optional_prefix;
my @opens = split //, $open_bracket;
my @closes = split //, $close_bracket;
my $prefix_index = 0;
my $open_index = 0;
my $close_index = 0;
my $result = '';
my $rest = '';
my $extracting = 0;
my $extracted = 0;
my $escaped = 0;
my $token = '';
my $ch = ' ';
my $last_ch;
my $i = 0;
my $bracket_pos;
my $bracket_level = 0;
my $prefix_group_match = @prefix_group ? 0 : 1;
my $prefix_match = @prefixes ? 0 : 1;
my $match = 0;
my @chars = split //, $string;
my $state = 'prefixgroup';
while (1) {
$last_ch = $ch;
if ($i >= @chars) {
if ($extracting) {
# reached end, but unbalanced brackets... reset to beginning and ignore them
$i = $bracket_pos;
$bracket_level = 0;
$state = 'prefixgroup';
$extracting = 0;
$last_ch = ' ';
$token = '';
$result = '';
} else {
# add final token and exit
$token .= '\\' if $escaped;
$rest .= $token if $extracted;
last;
}
}
$ch = $chars[$i++];
if ($escaped) {
$token .= "\\$ch" if $extracting or $extracted;
$escaped = 0;
next;
}
if ($ch eq '\\') {
$escaped = 1;
next;
}
if (not $extracted) {
if ($state eq 'prefixgroup' and @prefix_group and not $extracting) {
foreach my $prefix_ch (@prefix_group) {
if ($ch eq $prefix_ch) {
$prefix_group_match = 1;
$state = 'prefixes';
last;
} else {
$prefix_group_match = 0;
}
}
next if $prefix_group_match;
} elsif ($state eq 'prefixgroup' and not @prefix_group) {
$state = 'prefixes';
$prefix_index = 0;
}
if ($state eq 'prefixes') {
if (@prefixes and $ch eq $prefixes[$prefix_index]) {
$token .= $ch if $extracting;
$prefix_match = 1;
$prefix_index++;
$state = 'openbracket';
next;
} elsif ($state eq 'prefixes' and not @prefixes) {
$state = 'openbracket';
}
}
if ($extracting or ($state eq 'openbracket' and $prefix_group_match and $prefix_match)) {
$prefix_index = 0;
if ($ch eq $opens[$open_index]) {
$match = 1;
$open_index++;
} else {
if ($allow_whitespace and $ch eq ' ' and not $extracting) { next; }
elsif (not $extracting) {
$state = 'prefixgroup';
next;
}
}
}
if ($match) {
$state = 'prefixgroup';
$prefix_group_match = 0 unless not @prefix_group;
$prefix_match = 0 unless not @prefixes;
$match = 0;
$bracket_pos = $i if not $extracting;
if ($open_index == @opens) {
$extracting = 1;
$token .= $ch if $bracket_level > 0;
$bracket_level++;
$open_index = 0;
}
next;
} else {
$open_index = 0;
}
if ($ch eq $closes[$close_index]) {
if ($extracting or $extracted) {
$close_index++;
if ($close_index == @closes) {
$close_index = 0;
if (--$bracket_level == 0) {
$extracting = 0;
$extracted = 1;
$result .= $token;
$token = '';
} else {
$token .= $ch;
}
}
}
next;
} else {
$close_index = 0;
}
}
if ($extracting or $extracted) { $token .= $ch; }
}
return ($result, $rest);
}
# splits line into quoted arguments while preserving quotes.
# a string is considered quoted only if they are surrounded by
# whitespace or json separators.
# handles unbalanced quotes gracefully by treating them as
# part of the argument they were found within.
sub split_line {
my ($self, $line, %opts) = @_;
my %default_opts = (
strip_quotes => 0,
keep_spaces => 0,
preserve_escapes => 1,
strip_commas => 0,
);
%opts = (%default_opts, %opts);
return () if not length $line;
my @chars = split //, $line;
my @args;
my $escaped = 0;
my $quote;
my $token = '';
my $last_token = '';
my $ch = ' ';
my $last_ch;
my $next_ch;
my $i = 0;
my $pos;
my $ignore_quote = 0;
my $spaces = 0;
while (1) {
$last_ch = $ch;
if ($i >= @chars) {
if (defined $quote) {
# reached end, but unbalanced quote... reset to beginning of quote and ignore it
$i = $pos;
$ignore_quote = 1;
$quote = undef;
$last_ch = ' ';
$token = $last_token;
} else {
# add final token and exit
$token .= '\\' if $escaped;
push @args, $token if length $token;
last;
}
}
$ch = $chars[$i++];
$next_ch = $chars[$i];
$spaces = 0 if $ch ne ' ';
if ($escaped) {
if ($opts{preserve_escapes}) { $token .= "\\$ch"; }
else { $token .= $ch; }
$escaped = 0;
next;
}
if ($ch eq '\\') {
$escaped = 1;
next;
}
if (defined $quote) {
if ($ch eq $quote and (not defined $next_ch or $next_ch =~ /[\s,:;})\].+=]/)) {
# closing quote
$token .= $ch unless $opts{strip_quotes};
push @args, $token;
$quote = undef;
$token = '';
} else {
# still within quoted argument
$token .= $ch;
}
next;
}
if (($last_ch =~ /[\s:{(\[.+=,]/) and not defined $quote and ($ch eq "'" or $ch eq '"')) {
if ($ignore_quote) {
# treat unbalanced quote as part of this argument
$token .= $ch;
$ignore_quote = 0;
} else {
# begin potential quoted argument
$pos = $i - 1;
$quote = $ch;
$last_token = $token;
$token .= $ch unless $opts{strip_quotes};
}
next;
}
if ($ch eq ' ' or $ch eq "\n" or $ch eq "\t" or ($opts{strip_commas} and $ch eq ',')) {
if (++$spaces > 1 and $opts{keep_spaces}) {
$token .= $ch;
next;
} else {
unless ($opts{strip_commas} and $token eq ',') {
push @args, $token if length $token;
}
$token = '';
next;
}
}
$token .= $ch;
}
return @args;
}
# creates an array of arguments from a string
sub make_args {
my ($self, $string) = @_;
my @args = $self->split_line($string, keep_spaces => 1);
my @arglist;
my @arglist_unstripped;
while (@args) {
my $arg = shift @args;
# add argument with quotes and spaces preserved
push @arglist_unstripped, $arg;
# strip quotes from argument
if ($arg =~ m/^'.*'$/) {
$arg =~ s/^'//;
$arg =~ s/'$//;
} elsif ($arg =~ m/^".*"$/) {
$arg =~ s/^"//;
$arg =~ s/"$//;
}
# strip leading spaces from argument
$arg =~ s/^\s+//;
# add stripped argument
push @arglist, $arg;
}
# copy unstripped arguments to end of arglist
push @arglist, @arglist_unstripped;
return \@arglist;
}
# returns size of array of arguments
sub arglist_size {
my ($self, $args) = @_;
return @$args / 2;
}
# unshifts new argument to front
sub unshift_arg {
my ($self, $args, $arg) = @_;
splice @$args, @$args / 2, 0, $arg; # add quoted argument
unshift @$args, $arg; # add first argument
return @$args;
}
# shifts first argument off array of arguments
sub shift_arg {
my ($self, $args) = @_;
return undef if not @$args;
splice @$args, @$args / 2, 1; # remove original quoted argument
return shift @$args;
}
# returns list of unquoted arguments
sub unquoted_args {
my ($self, $args) = @_;
return undef if not @$args;
return @$args[0 .. @$args / 2 - 1];
}
# splits array of arguments into array with overflow arguments filling up last position
# split_args(qw/dog cat bird hamster/, 3) => ("dog", "cat", "bird hamster")
sub split_args {
my ($self, $args, $count, $offset, $preserve_quotes) = @_;
my @result;
my $max = $self->arglist_size($args);
$preserve_quotes //= 0;
my $i = $offset // 0;
unless ($count == 1) {
do {
my $arg = $args->[$i++];
push @result, $arg;
} while (--$count > 1 and $i < $max);
}
# join the get rest as a string
my $rest = '';
if ($preserve_quotes) {
# get from second half of args, which contains quotes
foreach my $arg (@$args[@$args / 2 + $i .. @$args - 1]) {
$rest .= ' ' unless not length $rest;
$rest .= $arg;
}
} else {
$rest = join ' ', @$args[$i .. $max - 1];
}
push @result, $rest if length $rest;
return @result;
}
# lowercases array of arguments
sub lc_args {
my ($self, $args) = @_;
for (my $i = 0; $i < @$args; $i++) { $args->[$i] = lc $args->[$i]; }
}
sub truncate_result {
my ($self, $from, $nick, $command, $paste_text, $text, $paste) = @_;
my $max_msg_len = $self->{pbot}->{registry}->get_value('irc', 'max_msg_len');
$max_msg_len -= length "PRIVMSG $from :" if defined $from;
utf8::encode $paste_text;
utf8::encode $text;
if (length $text > $max_msg_len) {
my $paste_result;
if ($paste) {
# limit pastes to 32k by default, overridable via paste.max_length
my $max_paste_len = $self->{pbot}->{registry}->get_value('paste', 'max_length') // 1024 * 32;
# truncate paste to max paste length
# FIXME: this potentially chops unicode characters in wrong places
$paste_text = substr $paste_text, 0, $max_paste_len;
utf8::decode $paste_text;
# send text to paste site
$paste_result = $self->{pbot}->{webpaste}->paste("($from) $nick: $command\n\n$paste_text");
}
my $trunc = "... [truncated";
if (not defined $paste_result) {
# no paste
$trunc .= "]";
} elsif ($paste_result =~ m/^http/) {
# a link
$trunc .= "; see $paste_result for full text.]";
} else {
# an error or something else
$trunc .= "$paste_result]";
}
if ($paste) {
$paste_result //= 'not pasted';
$self->{pbot}->{logger}->log("Message truncated -- $paste_result\n");
}
my $trunc_len = length $text < $max_msg_len ? length $text : $max_msg_len;
# FIXME: this potentially chops unicode characters in wrong places
$text = substr($text, 0, $trunc_len);
substr($text, $trunc_len - length $trunc) = $trunc;
}
utf8::decode $text;
return $text;
}
sub handle_result {
my ($self, $context, $result) = @_;
$result = $context->{result} if not defined $result;
$context->{preserve_whitespace} = 0 if not defined $context->{preserve_whitespace};
if ($self->{pbot}->{registry}->get_value('general', 'debugcontext') and length $context->{result}) {
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$self->{pbot}->{logger}->log("Interpreter::handle_result [$result]\n");
$self->{pbot}->{logger}->log(Dumper $context);
}
return 0 if not defined $result or length $result == 0;
if ($result =~ s#^(/say|/me) ##) { $context->{prepend} = $1; }
elsif ($result =~ s#^(/msg \S+) ##) { $context->{prepend} = $1; }
if (exists $context->{pipe}) {
my ($pipe, $pipe_rest) = (delete $context->{pipe}, delete $context->{pipe_rest});
if (not $context->{alldone}) {
$context->{command} = "$pipe $result $pipe_rest";
$result = $self->interpret($context);
$context->{result} = $result;
}
$self->handle_result($context, $result);
return 0;
}
if (exists $context->{subcmd}) {
my $command = pop @{$context->{subcmd}};
if (@{$context->{subcmd}} == 0 or $context->{alldone}) { delete $context->{subcmd}; }
$command =~ s/&\{subcmd\}/$result/;
if (not $context->{alldone}) {
$context->{command} = $command;
$result = $self->interpret($context);
$context->{result} = $result;
}
$self->handle_result($context);
return 0;
}
if ($context->{prepend}) { $result = "$context->{prepend} $result"; }
if ($context->{command_split}) {
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
$context->{command} = delete $context->{command_split};
$result =~ s#^/say #\n#i;
$result =~ s#^/me #\n* $botnick #i;
if (not length $context->{split_result}) {
$result =~ s/^\n//;
$context->{split_result} = $result;
} else {
$context->{split_result} .= $result;
}
$result = $self->interpret($context);
$self->handle_result($context, $result);
return 0;
}
if ($context->{split_result}) {
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
$result =~ s#^/say #\n#i;
$result =~ s#^/me #\n* $botnick #i;
$result = $context->{split_result} . $result;
}
my $use_output_queue = 0;
if (defined $context->{command}) {
my $cmdlist = $self->make_args($context->{command});
my ($cmd, $args) = $self->split_args($cmdlist, 2, 0, 1);
if (not $self->{pbot}->{commands}->exists($cmd)) {
my @factoids = $self->{pbot}->{factoids}->find_factoid($context->{from}, $cmd, arguments => $args, exact_channel => 0, exact_trigger => 0, find_alias => 1);
if (@factoids == 1) {
my ($chan, $trigger) = ($factoids[0]->[0], $factoids[0]->[1]);
if ($context->{preserve_whitespace} == 0) {
$context->{preserve_whitespace} = $self->{pbot}->{factoids}->{factoids}->get_data($chan, $trigger, 'preserve_whitespace') // 0;
}
$use_output_queue = $self->{pbot}->{factoids}->{factoids}->get_data($chan, $trigger, 'use_output_queue') // 0;
}
}
}
my $preserve_newlines = $self->{pbot}->{registry}->get_value($context->{from}, 'preserve_newlines');
my $original_result = $result;
$result =~ s/[\n\r]/ /g unless $preserve_newlines;
$result =~ s/[ \t]+/ /g unless $context->{preserve_whitespace};
my $max_lines = $self->{pbot}->{registry}->get_value($context->{from}, 'max_newlines') // 4;
my $lines = 0;
my $stripped_line;
foreach my $line (split /[\n\r]+/, $result) {
$stripped_line = $line;
$stripped_line =~ s/^\s+|\s+$//g;
next if not length $stripped_line;
if (++$lines >= $max_lines) {
my $link = $self->{pbot}->{webpaste}->paste("[" . (defined $context->{from} ? $context->{from} : "stdin") . "] <$context->{nick}> $context->{text}\n\n$original_result");
if ($use_output_queue) {
my $message = {
nick => $context->{nick}, user => $context->{user}, host => $context->{host}, command => $context->{command},
message => "And that's all I have to say about that. See $link for full text.",
checkflood => 1
};
$self->add_message_to_output_queue($context->{from}, $message, 0);
} else {
$self->{pbot}->{conn}->privmsg($context->{from}, "And that's all I have to say about that. See $link for full text.") unless $context->{from} eq 'stdin@pbot';
}
last;
}
if ($preserve_newlines) {
$line = $self->truncate_result($context->{from}, $context->{nick}, $context->{text}, $line, $line, 1);
} else {
$line = $self->truncate_result($context->{from}, $context->{nick}, $context->{text}, $original_result, $line, 1);
}
if ($use_output_queue) {
my $delay = rand(10) + 5;
my $message = {
nick => $context->{nick},
user => $context->{user},
host => $context->{host},
command => $context->{command},
message => $line,
checkflood => 1,
};
$self->add_message_to_output_queue($context->{from}, $message, $delay);
$delay = duration($delay);
$self->{pbot}->{logger}->log("($delay delay) $line\n");
} else {
$context->{line} = $line;
$self->output_result($context);
$self->{pbot}->{logger}->log("$line\n");
}
}
$self->{pbot}->{logger}->log("---------------------------------------------\n");
return 1;
}
sub dehighlight_nicks {
my ($self, $line, $channel) = @_;
return $line if $self->{pbot}->{registry}->get_value('general', 'no_dehighlight_nicks');
my @tokens = split / /, $line;
foreach my $token (@tokens) {
my $potential_nick = $token;
$potential_nick =~ s/^[^\w\[\]\-\\\^\{\}]+//;
$potential_nick =~ s/[^\w\[\]\-\\\^\{\}]+$//;
next if length $potential_nick == 1;
next if not $self->{pbot}->{nicklist}->is_present($channel, $potential_nick);
my $dehighlighted_nick = $potential_nick;
$dehighlighted_nick =~ s/(.)/$1\x{200b}/;
$token =~ s/\Q$potential_nick\E(?!:)/$dehighlighted_nick/;
}
return join ' ', @tokens;
}
sub output_result {
my ($self, $context) = @_;
my ($pbot, $botnick) = ($self->{pbot}, $self->{pbot}->{registry}->get_value('irc', 'botnick'));
if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) {
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$self->{pbot}->{logger}->log("Interpreter::output_result\n");
$self->{pbot}->{logger}->log(Dumper $context);
}
my $line = $context->{line};
return if not defined $line or not length $line;
return 0 if $context->{from} eq 'stdin@pbot';
$line = $self->dehighlight_nicks($line, $context->{from}) if $context->{from} =~ /^#/ and $line !~ /^\/msg\s+/i;
if ($line =~ s/^\/say(?:\s+|$)//i) {
$line = ' ' if not length $line;
if (defined $context->{nickoverride} and ($context->{no_nickoverride} == 0 or $context->{force_nickoverride} == 1)) { $line = "$context->{nickoverride}: $line"; }
$pbot->{conn}->privmsg($context->{from}, $line) if defined $context->{from} && $context->{from} ne $botnick;
$pbot->{antiflood}->check_flood($context->{from}, $botnick, $pbot->{registry}->get_value('irc', 'username'), 'pbot', $line, 0, 0, 0) if $context->{checkflood};
} elsif ($line =~ s/^\/me\s+//i) {
$pbot->{conn}->me($context->{from}, $line) if defined $context->{from} && $context->{from} ne $botnick;
$pbot->{antiflood}->check_flood($context->{from}, $botnick, $pbot->{registry}->get_value('irc', 'username'), 'pbot', '/me ' . $line, 0, 0, 0) if $context->{checkflood};
} elsif ($line =~ s/^\/msg\s+([^\s]+)\s+//i) {
my $to = $1;
if ($to =~ /,/) {
$pbot->{logger}->log("[HACK] Possible HACK ATTEMPT /msg multiple users: [$context->{nick}!$context->{user}\@$context->{host}] [$context->{command}] [$line]\n");
} elsif ($to =~ /.*serv(?:@.*)?$/i) {
$pbot->{logger}->log("[HACK] Possible HACK ATTEMPT /msg *serv: [$context->{nick}!$context->{user}\@$context->{host}] [$context->{command}] [$line]\n");
} elsif ($line =~ s/^\/me\s+//i) {
$pbot->{conn}->me($to, $line) if $to ne $botnick;
$pbot->{antiflood}->check_flood($to, $botnick, $pbot->{registry}->get_value('irc', 'username'), 'pbot', '/me ' . $line, 0, 0, 0) if $context->{checkflood};
} else {
$line =~ s/^\/say\s+//i;
if (defined $context->{nickoverride} and ($context->{no_nickoverride} == 0 or $context->{force_nickoverride} == 1)) { $line = "$context->{nickoverride}: $line"; }
$pbot->{conn}->privmsg($to, $line) if $to ne $botnick;
$pbot->{antiflood}->check_flood($to, $botnick, $pbot->{registry}->get_value('irc', 'username'), 'pbot', $line, 0, 0, 0) if $context->{checkflood};
}
} else {
if (defined $context->{nickoverride} and ($context->{no_nickoverride} == 0 or $context->{force_nickoverride} == 1)) { $line = "$context->{nickoverride}: $line"; }
$pbot->{conn}->privmsg($context->{from}, $line) if defined $context->{from} && $context->{from} ne $botnick;
$pbot->{antiflood}->check_flood($context->{from}, $botnick, $pbot->{registry}->get_value('irc', 'username'), 'pbot', $line, 0, 0, 0) if $context->{checkflood};
}
}
sub add_message_to_output_queue {
my ($self, $channel, $message, $delay) = @_;
$self->{pbot}->{timer}->enqueue_event(
sub {
my $context = {
from => $channel,
nick => $message->{nick},
user => $message->{user},
host => $message->{host},
line => $message->{message},
command => $message->{command},
checkflood => $message->{checkflood}
};
$self->output_result($context);
},
$delay, "output $channel $message->{message}"
);
}
sub add_to_command_queue {
my ($self, $channel, $command, $delay, $repeating) = @_;
$self->{pbot}->{timer}->enqueue_event(
sub {
my $context = {
from => $channel,
nick => $command->{nick},
user => $command->{user},
host => $command->{host},
command => $command->{command},
interpret_depth => 0,
checkflood => 0,
preserve_whitespace => 0
};
if (exists $command->{'cap-override'}) {
$self->{pbot}->{logger}->log("[command queue] Override command capability with $command->{'cap-override'}\n");
$context->{'cap-override'} = $command->{'cap-override'};
}
my $result = $self->interpret($context);
$context->{result} = $result;
$self->handle_result($context, $result);
},
$delay, "command $channel $command->{command}", $repeating
);
}
sub add_botcmd_to_command_queue {
my ($self, $channel, $command, $delay) = @_;
my $botcmd = {
nick => $self->{pbot}->{registry}->get_value('irc', 'botnick'),
user => 'stdin',
host => 'pbot',
command => $command
};
$self->add_to_command_queue($channel, $botcmd, $delay);
}
1;