# File: Variables.pm # # Purpose: Implements factoid variables, including $args, $nick, $channel, etc. # SPDX-FileCopyrightText: 2005-2023 Pragmatic Software # SPDX-License-Identifier: MIT package PBot::Core::Factoids::Variables; use parent 'PBot::Core::Class'; use PBot::Imports; use PBot::Core::Utils::Indefinite; use PBot::Core::Utils::ValidateString; use JSON; sub initialize {} sub expand_factoid_vars($self, $context, $action, %opts) { my %default_opts = ( nested => 0, recursions => 0, ); %opts = (%default_opts, %opts); if (++$opts{recursions} > 100) { return '!recursion limit reached!'; } 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->{pbot}->{factoids}->{data}->{storage}->get_data($context->{channel}, $context->{keyword}, 'interpolate'); return $action if defined $interpolate and not $interpolate; $interpolate = $self->{pbot}->{registry}->get_value($context->{channel}, 'interpolate_factoids'); return $action if defined $interpolate and not $interpolate; $action = $self->{pbot}->{factoids}->{selectors}->expand_selectors($context, $action, %opts); my $depth = 0; if ($action =~ m/^\/call --keyword-override=([^ ]+)/i) { $root_keyword = $1; } my $result = ''; my $rest = $action; while (++$depth < 100) { $rest =~ s/(?{pbot}->{interpreter}->extract_bracketed($rest, '{', '}'); $orig_var = $var; if ($var =~ /:/) { my @stuff = split /:/, $var, 2; $var = $stuff[0]; $modifiers = ':' . $stuff[1]; } $extract_method = 'bracket'; } else { $rest =~ s/^(\w+)//; $var = $orig_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' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } $matches++; # extract channel expansion modifier if ($var =~ s/:(#[^: ]+|global)//i || $rest =~ s/^:(#[^: ]+|global)//i) { $from = $1; $from = '.*' if lc $from eq 'global'; } my $recurse = 0; ALIAS: my @factoids = $self->{pbot}->{factoids}->{data}->find($from, $var, exact_channel => 2, exact_trigger => 2); if (not @factoids or not $factoids[0]) { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } my $var_chan; ($var_chan, $var) = ($factoids[0]->[0], $factoids[0]->[1]); if ($self->{pbot}->{factoids}->{data}->{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' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } goto ALIAS; } if ($modifiers) { $rest = $modifiers . $rest; } my $copy = $rest; my %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$copy); if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'type') eq 'text') { my $change = $self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action'); my @list = $self->{pbot}->{interpreter}->split_line($change); my @replacements; if (wantarray) { @replacements = $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), \$rest, %opts); return @replacements; } else { push @replacements, scalar $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), \$rest, %opts); } my $replacement = $opts{nested} ? join('|', @replacements) : "@replacements"; if (not length $replacement) { $result =~ s/\s+$//; } else { $replacement = $self->{pbot}->{factoids}->{variables}->expand_factoid_vars($context, $replacement, %opts); } 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; $expansions++; } else { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; } } 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/(?{pbot}->{registry}->get_value('factoids', 'max_content_length')); } sub expand_action_arguments($self, $action, $input, $nick) { $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')); my %h; if (not defined $input or $input eq '') { %h = (args => $nick); } else { %h = (args => $input); } my $jsonargs = to_json \%h; $jsonargs =~ s/^{".*":"//; $jsonargs =~ s/"}$//; 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; } my @args = $self->{pbot}->{interpreter}->split_line($input); $action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg; 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; } if ($arg =~ m/([^:]*):(.*)/) { my $arg1 = $1; my $arg2 = $2; my $arg1i = $arg1; my $arg2i = $arg2; $arg1i = 0 if $arg1i eq ''; $arg2i = $#args if $arg2i eq ''; $arg2i = $#args if $arg2i > $#args; 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/; } } } return $action; } sub escape_json($self, $text) { my $thing = {thing => $text}; my $json = to_json $thing; $json =~ s/^{".*":"//; $json =~ s/"}$//; return $json; } sub expand_special_vars($self, $from, $nick, $root_keyword, $action) { $action =~ s/(?escape_json($nick)/ge; $action =~ s/(?escape_json($from)/ge; $action =~ s/(?{pbot}->{nicklist}->random_nick($from); $random ? $self->escape_json($random) : $self->escape_json($nick)/ge; $action =~ s/(?escape_json($root_keyword)/ge; $action =~ s/(?{pbot}->{nicklist}->random_nick($from); $random ? $random : $nick/ge; $action =~ s/(?{pbot}->{registry}->get_value('factoids', 'max_content_length')); } 1;