# 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); my $var_chan; if (@factoids && $factoids[0]) { ($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; } } my $copy; my $bracketed = 0; if ($extract_method eq 'bracket') { $copy = $modifiers; $bracketed = 1; } else { $copy = $rest; } my %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$copy, $bracketed); my $change; if (not @factoids or not $factoids[0]) { if (exists $settings{default} && length $settings{default}) { $change = $settings{default}; } else { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } } else { if ($self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'type') eq 'text') { $change = $self->{pbot}->{factoids}->{data}->{storage}->get_data($var_chan, $var, 'action'); } } if (defined $change) { my @list = $self->{pbot}->{interpreter}->split_line($change); my @replacements; my $ref; if ($extract_method eq 'bracket') { $ref = \$modifiers; } else { $ref = \$rest; } if (wantarray) { @replacements = $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), $ref, %opts); return @replacements; } else { push @replacements, scalar $self->{pbot}->{factoids}->{selectors}->select_item($context, join ('|', @list), $ref, %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, $context, $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 %opts = ( nested => 0, ); my @args = $self->{pbot}->{interpreter}->split_line($input); $action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg; my $root_keyword = $context->{keyword_override} ? $context->{keyword_override} : $context->{root_keyword}; if ($action =~ m/^\/call --keyword-override=([^ ]+)/i) { $root_keyword = $1; } my $depth = 0; my $const_action = $action; 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 ne 'args' && $var ne 'arg') { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } $matches++; my $value; my $use_nick = 0; if ($var eq 'args') { if (!defined $input || !length $input) { $value = undef; $use_nick = 1; } else { $value = $input; } } else { my $index; if ($rest =~ s/\[(.*?)\]//) { $index = $1; } else { $result .= $extract_method eq 'bracket' ? '${' . $orig_var . '}' : '$' . $orig_var; next; } if ($index eq '*') { if (!defined $input || !length $input) { $value = undef; $use_nick = 1; } else { $value = $input; } } elsif ($index =~ 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 ($@) { $value = undef; } else { $value = join(' ', @values); } } else { $value = eval { local $SIG{__WARN__} = sub { }; return $args[$index]; }; if ($@) { if ($index == 0) { $value = $nick; } else { $value = undef; } } if (!defined $value) { if ($index == 0) { $use_nick = 1; } } } } my %settings; if ($extract_method eq 'bracket') { %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$modifiers, 1); } else { %settings = $self->{pbot}->{factoids}->{modifiers}->parse(\$rest); } my $change; if (!defined $value || !length $value) { if (exists $settings{default} && length $settings{default}) { $change = $settings{default}; } else { if ($use_nick) { $change = $nick; } else { $change = ''; } } } else { $change = $value; } if (defined $change) { if (not length $change) { $result =~ s/\s+$//; } if ($settings{'uc'}) { $change = uc $change; } if ($settings{'lc'}) { $change = lc $change; } if ($settings{'ucfirst'}) { $change = ucfirst $change; } if ($settings{'title'}) { $change = ucfirst lc $change; $change =~ s/ (\w)/' ' . uc $1/ge; } if ($settings{'json'}) { $change = $self->escape_json($change); } if ($result =~ s/\b(a|an)(\s+)$//i) { my ($article, $trailing) = ($1, $2); my $fixed_article = select_indefinite_article $change; if ($article eq 'AN') { $fixed_article = uc $fixed_article; } elsif ($article eq 'An' or $article eq 'A') { $fixed_article = ucfirst $fixed_article; } $change = $fixed_article . $trailing . $change; } $result .= $change; $expansions++; } } last if $matches == 0 or $expansions == 0; if (not length $rest) { $rest = $result; $result = ''; } } $result .= $rest; # unescape certain symbols $result =~ s/(?{pbot}->{registry}->get_value('factoids', 'max_content_length')); } 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;