1
0
Fork 0
mirror of https://github.com/perlbot/perlbuut synced 2025-06-07 18:25:42 -04:00
perlbuut/plugins/factoids.pm

1133 lines
45 KiB
Perl

package Bot::BB3::Plugin::Factoids;
use v5.30;
use experimental 'signatures';
use feature 'postderef', 'fc';
use DBI;
use IRC::Utils qw/lc_irc strip_color strip_formatting/;
use Text::Metaphone;
use strict;
use Encode qw/decode/;
use JSON::MaybeXS qw/encode_json/;
use Data::Dumper;
use List::Util qw/min max/;
open(my $fh, "<", "etc/factoid_db_keys") or die $!;
my ($dbname, $dbuser, $dbpass) = <$fh>;
close($fh);
chomp $dbname;
chomp $dbuser;
chomp $dbpass;
#############################
# BIG WARNING ABOUT THE DATABASE IN HERE.
#############################
#
# Despite the name 'original_subject' and 'subject' are logically reversed, e.g. 'original_subject' contains the cleaned up and filtered subject rather than the other way around.
# This should be kept in mind when working on any and all of the code below
# --simcop2387 (previously also discovered by buu, but not documented or fixed).
#
# This might be fixed later but for now its easier to just "document" it. (boy doesn't this feel enterprisy!)
#
#############################
my $COPULA = join '|', qw/is are was isn't were being am/, "to be", "will be", "has been", "have been", "shall be", "can has", "wus liek", "iz liek", "used to be";
my $COPULA_RE = qr/\b(?:$COPULA)\b/i;
#this is a hash that gives all the commands their names and functions, added to avoid some symbol table funkery that happened originally.
my %commandhash = (
# "" => \&get_fact, #don't ever add the default like this, it'll cause issues! i plan on changing that!
"forget" => \&get_fact_forget,
"delete" => \&get_fact_delete,
"learn" => \&get_fact_learn,
"relearn" => \&get_fact_learn,
"literal" => \&get_fact_literal,
"revert" => \&get_fact_revert,
"revisions" => \&get_fact_revisions,
"search" => \&get_fact_search,
"oldsearch" => \&get_fact_oldsearch,
"protect" => \&get_fact_protect,
"unprotect" => \&get_fact_unprotect,
"substitute" => \&get_fact_substitute,
"nchain" => \&get_fact_namespace_chain,
"factgrep" => \&get_fact_grep,
);
my $commands_re = join '|', keys %commandhash;
$commands_re = qr/$commands_re/;
sub new($class) {
my $self = bless {}, $class;
$self->{name} = 'factoids'; # Shouldn't matter since we aren't a command
$self->{opts} = {
command => 1,
handler => 1,
};
$self->{aliases} = [qw/fact call nfacts/];
return $self;
}
sub dbh($self) {
if ($self->{dbh} and $self->{dbh}->ping) {
return $self->{dbh};
}
my $dbh = $self->{dbh} =
DBI->connect("dbi:Pg:dbname=$dbname;host=192.168.32.1", $dbuser, $dbpass, { RaiseError => 1, PrintError => 0 });
return $dbh;
}
sub get_namespace($self, $said) {
my ($server, $channel) = $said->@{qw/server channel/};
$server =~ s/^.*?([^\.]+\.[^\.]+)$/$1/;
return ($server, $channel);
}
sub get_alias_namespace($self, $said) {
my $conf = $self->get_conf_for_channel($said);
my $server = $conf->{alias_server};
my $namespace = $conf->{alias_namespace};
return ($server, $namespace);
}
sub get_conf_for_channel ($self, $said) {
my ($server, $namespace) = $self->get_namespace($said);
my $dbh = $self->dbh;
my $result = $dbh->selectrow_hashref(qq{
SELECT * FROM public.factoid_config WHERE server = ? AND namespace = ? LIMIT 1
}, {}, $server, $namespace);
my $conf = {
server => '',
namespace => '',
alias_server => '',
alias_namespace => '',
parent_server => undef,
parent_namespace => undef,
recursive => 0,
command_prefix => undef,
%{$result//{}},
};
return $conf;
}
# TODO update this to use the new table layout once it's ready
sub postload {
my ($self, $pm) = @_;
# my $sql = "CREATE TABLE factoid (
# factoid_id INTEGER PRIMARY KEY AUTOINCREMENT,
# original_subject VARCHAR(100),
# subject VARCHAR(100),
# copula VARCHAR(25),
# predicate TEXT,
# author VARCHAR(100),
# modified_time INTEGER,
# metaphone TEXT,
# compose_macro CHAR(1) DEFAULT '0',
# protected BOOLEAN DEFAULT '0'
# );
# CREATE INDEX factoid_subject_idx ON factoid(subject);
# CREATE INDEX factoid_original_subject_idx ON factoid(original_subject_idx);
# "; # Stupid lack of timestamp fields
#
# $pm->create_table( $self->dbh, "factoid", $sql );
#
# delete $self->{dbh}; # UGLY HAX GO.
# Basically we delete the dbh we cached so we don't fork
# with one active
}
# This whole code is a mess.
# Essentially we need to check if the user's text either matches a
# 'store command' such as "subject is predicate" or we need to check
# if it's a retrieve command such as "foo" or if it's a retrieve sub-
# command such as "forget foo"
# Need to add "what is foo?" support...
sub command ($self, $_said, $pm) {
my $said = +{ $_said->%* };
if ($said->{channel} eq '*irc_msg') {
# Parse body here
my $body = $said->{body};
$said->{channel} = "##NULL" if $said->{channel} eq '*irc_msg';
}
if ($said->{body} =~ /^\s*(?<channel>#\S+)\s+(?<fact>.*)$/) {
$said->{channel} = $+{channel};
$said->{body} = $+{fact};
}
# TODO does this need to support parsing the command out again?
my ($handled, $fact_out) = $self->sub_command($said, $pm);
return ($handled, $fact_out);
}
sub sub_command ($self, $said, $pm) {
return unless $said->{body} =~ /\S/; #Try to prevent "false positives"
my $call_only = $said->{command_match} eq "call";
my $subject = $said->{body};
my $commands_re = join '|', keys %commandhash;
$commands_re = qr/$commands_re/;
my $fact_string; # used to capture return values
if (!$call_only && $subject =~ s/^\s*($commands_re)\s*//) {
$fact_string =
$commandhash{$1}->($self, $subject, $said->{name}, $said);
} elsif (($subject =~ m{\w\s*=~\s*s /.+ / .* /[gi]*\s*$}ix)
|| ($subject =~ m{\w\s*=~\s*s\|.+\| .*\|[gi]*\s*$}ix)
|| ($subject =~ m{\w\s*=~\s*s\{.+\}\{.*\}[gi]*\s*$}ix)
|| ($subject =~ m{\w\s*=~\s*s <.+ > <.* >[gi]*\s*$}ix)
|| ($subject =~ m{\w\s*=~\s*s\(.+\)\(.*\)[gi]*\s*$}ix))
{
$fact_string = $self->get_fact_substitute($subject, $said->{name}, $said);
} elsif (!$call_only and $subject =~ /\s+$COPULA_RE\s+/) {
return if $said->{nolearn};
my @ret = $self->store_factoid($said);
$fact_string = "Failed to store $said->{body}" unless @ret;
$fact_string = "@ret" if ($ret[0] =~ /^insuff/i);
$fact_string = "Stored @ret";
} else {
$fact_string = $self->get_fact($pm, $said, $subject, $said->{name}, $call_only);
}
if (defined $fact_string) {
return ('handled', $fact_string);
} else {
return;
}
}
# Handler code stolen from the old nfacts plugin
sub handle ($self, $said, $pm) {
my $conf = $self->get_conf_for_channel($said);
$said->{body} =~ s/^\s*(what|who|where|how|when|why)\s+($COPULA_RE)\s+(?<fact>.*?)\??\s*$/$+{fact}/i;
my $prefix = $conf->{command_prefix};
return unless $prefix;
$said->{nosuggest} = 1;
# TODO make this channel configurable and make it work properly to learn shit with colors later.
$said->{body} = strip_formatting strip_color $said->{body};
if ( $said->{body} =~ /^\Q$prefix\E(?<fact>[^@]*?)(?:\s@\s*(?<user>\S*)\s*)?$/
|| $said->{body} =~ /^\Q$prefix\E!@(?<user>\S+)\s+(?<fact>.+)$/)
{
my $fact = $+{fact};
my $user = $+{user};
my $newsaid = +{ $said->%* };
$newsaid->{body} = $fact;
if ($fact =~ /^\s*(?<channel>#\S+)\s+(?<fact>.*)$/) {
my ($fact, $channel) = @+{qw/fact channel/};
$newsaid->{body} = $fact;
$newsaid->{channel} = $channel;
}
$newsaid->{addressed} = 1;
$newsaid->{nolearn} = 1;
my ($s, $r) = $self->command($newsaid, $pm);
if ($s) {
$r = "$user: $r" if $user;
$r = "\0" . $r;
return ($r, 'handled');
}
}
return;
}
sub _clean_subject($subject) {
$subject =~ s/^\s+//;
$subject =~ s/\s+$//;
$subject =~ s/\s+/ /g;
# $subject =~ s/[^\w\s]//g; #comment out to fix punct in factoids
$subject = lc fc $subject;
return $subject;
}
# TODO document this better
sub _clean_subject_func ($subject, $variant) { # for parametrized macros
my ($key, $arg);
if ($variant) {
$subject =~ /\A\s*(\S+(?:\s+\S+)?)(?:\s+(.*))?\z/s or return;
($key, $arg) = ($1, $2);
} else {
$subject =~ /\A\s*(\S+)(?:\s+(.*))?\z/s or return;
($key, $arg) = ($1, $2);
}
return $key, $arg;
}
sub store_factoid ($self, $said) {
my ($self, $said) = @_;
# alias namespace is the current alias we assign factoids to
# server and namespace is the server and channel we're looking up for
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
my ($author, $body) = ($said->{name}, $said->{body});
return unless $body =~ /^(?:no[, ])?\s*(.+?)\s+($COPULA_RE)\s+(.+)$/s;
my ($subject, $copula, $predicate) = ($1, $2, $3);
my $compose_macro = 0;
return "Insufficient permissions for changing protected factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
if ($subject =~ s/^\s*\@?macro\b\s*//) {$compose_macro = 1;}
elsif ($subject =~ s/^\s*\@?func\b\s*//) {$compose_macro = 2;}
elsif ($predicate =~ s/^\s*also\s+//) {
my $fact = $self->_db_get_fact(_clean_subject($subject), 0, $server, $namespace);
$predicate = $fact->{predicate} . " | " . $predicate;
}
return
unless $self->_insert_factoid($author, $subject, $copula, $predicate, $compose_macro, $self->_db_get_protect($subject, $server, $namespace), $aliasserver, $aliasnamespace);
return ($subject, $copula, $predicate);
}
sub _insert_factoid ($self, $author, $subject, $copula, $predicate, $compose_macro, $protected, $server, $namespace, $deleted=0) {
my $dbh = $self->dbh;
warn "Attempting to insert factoid: type $compose_macro";
my $key;
if ($compose_macro == 2) {
($key, my $arg) = _clean_subject_func($subject, 1);
warn "*********************** GENERATED [$key] FROM [$subject] and [$arg]\n";
$arg =~ /\S/
and return;
} else {
$key = _clean_subject($subject);
}
return unless $key =~ /\S/;
$dbh->do(
"INSERT INTO factoid
(original_subject,subject,copula,predicate,author,modified_time,metaphone,compose_macro,protected, namespace, server, deleted,last_rendered)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
undef,
$key,
$subject,
$copula,
$predicate,
lc_irc($author),
time,
Metaphone($key),
$compose_macro || 0,
$protected || 0,
$namespace,
$server,
$deleted,
$predicate
);
# TODO trigger FTS update?
return 1;
}
sub get_fact_protect ($self, $subject, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
warn "===TRYING TO PROTECT [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for protecting factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
my $fact = $self->_db_get_fact(_clean_subject($subject), 0, $server, $namespace);
if (defined($fact->{predicate})) {
$self->_insert_factoid($name, $subject, $fact->{copula}, $fact->{predicate}, $fact->{compose_macro}, 1, $aliasserver, $aliasnamespace);
return "Protected [$subject]";
} else {
return "Unable to protect nonexisting factoid [$subject]";
}
}
sub get_fact_unprotect ($self, $subject, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
warn "===TRYING TO PROTECT [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for unprotecting factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
my $fact = $self->_db_get_fact(_clean_subject($subject), 0, $server, $namespace);
if (defined($fact->{predicate})) {
$self->_insert_factoid($name, $subject, $fact->{copula}, $fact->{predicate}, $fact->{compose_macro}, 0, $aliasserver, $aliasnamespace);
return "Unprotected [$subject]";
} else {
return "Unable to unprotect nonexisting factoid [$subject]";
}
}
sub get_fact_forget ($self, $subject, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
warn "===TRYING TO FORGET [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for forgetting protected factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
$self->_insert_factoid($name, $subject, "is", " ", 0, $self->_db_get_protect($subject, $server, $namespace), $aliasserver, $aliasnamespace);
return "Forgot $subject";
}
sub get_fact_delete ($self, $subject, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
warn "===TRYING TO DELETE [$subject] [$name]\n";
#XXX check permissions here
return "Insufficient permissions for deleting protected factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
$self->_insert_factoid($name, $subject, "is", " ", 0, $self->_db_get_protect($subject, $server, $namespace), $aliasserver, $aliasnamespace, 1);
return "Deleted $subject from $server:$namespace";
}
sub _fact_literal_format($r, $aliasserver, $aliasnamespace) {
$aliasserver ||= "*";
$aliasnamespace ||= "##NULL";
# TODO make this express the parent namespace if present
# <server:namespace>
#
(($aliasserver eq ($r->{generated_server}||"*") && $aliasnamespace eq ($r->{generated_namespace}||"##NULL")) ? "" : sprintf("<%s:%s> ", $r->{generated_server}||"*", $r->{generated_namespace}||"##NULL"))
. ($r->{deleted} ? "[REDACTED]" :
(
($r->{protected} ? "P:" : "")
. ("", "macro ", "func ")[$r->{compose_macro}]
. "$r->{subject} $r->{copula} $r->{predicate}"
));
}
sub get_fact_revisions ($self, $subject, $name, $said) {
my $dbh = $self->dbh;
my ($server, $namespace) = $self->get_namespace($said);
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
# TODO this query should use the deleted flag to figure out
# which depth lookup should be valid at any given time
# but that's a much more complicated query i don't want to make
# maybe just do it in perl later
my $revisions = $dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject = ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT * FROM get_latest_factoid ORDER BY factoid_id DESC;
", # newest revision first
{ Slice => {} },
$namespace, $server,
_clean_subject($subject),
);
my $ret_string = join " \n", map {"[$_->{factoid_id} by $_->{author}: " . _fact_literal_format($_, $aliasserver, $aliasnamespace) . "]";} @$revisions;
return $ret_string;
}
sub get_fact_literal ($self, $subject, $name, $said) {
my ($server, $namespace) = $self->get_namespace($said);
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
print STDERR "literal parse: $subject, $name, $server, $namespace\n";
my $fact = $self->_db_get_fact(_clean_subject($subject), 0, $server, $namespace);
print STDERR "literal fact: ".Dumper($fact)."\n";
my $formatted = _fact_literal_format($fact, $aliasserver, $aliasnamespace);
print STDERR "formatted: $formatted\n";
return $formatted;
}
sub _fact_substitute ($self, $pred, $match, $subst, $flags) {
if ($flags =~ /g/) {
my $regex = $flags =~ /i/ ? qr/(?i:$match)/i : qr/$match/;
while ($pred =~ /$regex/g) {
my $matchedstring = substr($pred, $-[0], $+[0] - $-[0]);
my ($matchstart, $matchend) = ($-[0], $+[0]);
my @caps =
map {substr($pred, $-[$_], $+[$_] - $-[$_])} 1 .. $#+;
my $realsubst = $subst;
$realsubst =~ s/(?<!\\)\$(?:\{(\d+)\}|(\d+))/$caps[$1-1]/eg;
$realsubst =~ s/\\(?=\$)//g;
substr $pred, $matchstart, $matchend - $matchstart, $realsubst;
pos $pred = $matchstart + length($realsubst); #set the new position, might have an off by one?
}
return $pred;
} else {
my $regex = $flags =~ /i/ ? qr/(?i:$match)/i : qr/$match/;
if ($pred =~ /$regex/) {
my @caps =
map {substr($pred, $-[$_], $+[$_] - $-[$_])} 1 .. $#+;
my $realsubst = $subst;
$realsubst =~ s/(?<!\\)\$(?:\{(\d+)\}|(\d+))/$caps[$1-1]/eg;
$realsubst =~ s/\\(?=\$)//g;
$pred =~ s/$regex/$realsubst/;
}
return $pred;
}
}
sub get_fact_substitute ($self, $subject, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
if ( ($said->{body} =~ m{^(?:\s*substitute)?\s*(.*?)\s*=~\s*s /([^/]+ ) /([^/]* )/([gi]*)\s*$}ix)
|| ($said->{body} =~ m{^(?:\s*substitute)?\s*(.*?)\s*=~\s*s\|([^|]+ ) \|([^|]* )\|([gi]*)\s*$}ix)
|| ($said->{body} =~ m{^(?:\s*substitute)?\s*(.*?)\s*=~\s*s\{([^\}]+)\}\{([^\}]*?)\}([gi]*)\s*$}ix)
|| ($said->{body} =~ m{^(?:\s*substitute)?\s*(.*?)\s*=~\s*s\(([^)]+ )\)\(([^)]*? )\)([gi]*)\s*$}ix)
|| ($said->{body} =~ m{^(?:\s*substitute)?\s*(.*?)\s*=~\s*s <([^>]+ ) > <([^>]*? ) >([gi]*)\s*$}ix))
{
my ($subject, $match, $subst, $flags) = ($1, $2, $3, $4);
# TODO does this need to be done via the ->get_fact() instead now?
my $fact = $self->_db_get_fact(_clean_subject($subject), 0, $server, $namespace);
if ($fact && $fact->{predicate} =~ /\S/) { #we've got a fact to operate on
if ($match !~ /(?:\(\?\??\{)/) { #ok, match has checked out to be "safe", this will likely be extended later
my $pred = $fact->{predicate};
my $result;
#moving this to its own function for cleanliness
$result = $self->_fact_substitute($pred, $match, $subst, $flags);
# my( $self, $body, $name, $said ) = @_;
# $body =~ s/^\s*learn\s+//;
# my( $subject, $predicate ) = split /\s+as\s+/, $body, 2;
# TODO why is this calling there?
# let this fail for now
my $ret = $self->get_fact_learn("learn $subject as $result", $name, $said, $subject, $result);
return $ret;
} else {
return "Can't use dangerous things in a regex, you naughty user";
}
} else {
return "Can't substitute on unknown factoid [$subject]";
}
}
}
sub get_fact_revert ($self, $subject, $name, $said) {
my $dbh = $self->dbh;
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
#XXX check permissions here
return "Insufficient permissions for reverting protected factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
$subject =~ s/^\s*(\d+)\s*$//
or return "Failed to match revision format";
my $rev_id = $1;
my $fact_rev = $dbh->selectrow_hashref(
"SELECT subject, copula, predicate, compose_macro
FROM factoid
WHERE factoid_id = ?",
undef,
$rev_id
);
my $protect = $self->_db_get_protect($fact_rev->{subject}, $server, $namespace);
return "Bad revision id"
unless $fact_rev and $fact_rev->{subject}; # Make sure it's valid..
# subject, copula, predicate
$self->_insert_factoid($name, @$fact_rev{qw"subject copula predicate compose_macro"}, $protect, $aliasserver, $aliasnamespace);
return "Reverted $fact_rev->{subject} to revision $rev_id";
}
sub get_fact_learn ($self, $body, $name, $said, $subject=undef, $predicate=undef) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
return if ($said->{nolearn});
$body =~ s/^\s*learn\s+//;
my $copula = "is";
unless ($subject && $predicate) {
($subject, $copula, $predicate) = $body =~ /^\s*(.*?)\s+(as|$COPULA_RE)\s+(.*)\s*$/ig;
}
#XXX check permissions here
return "Insufficient permissions for changing protected factoid [$subject]"
if (!$self->_db_check_perm($subject, $said));
#my @ret = $self->store_factoid( $name, $said->{body} );
$self->_insert_factoid($name, $subject, $copula, $predicate, 0, $self->_db_get_protect($subject, $server, $namespace), $aliasserver, $aliasnamespace);
return "Stored $subject as $predicate";
}
sub get_fact_search($self, $body, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
$body =~ s/^\s*for\s*//; #remove the for from searches
my $results = $self->dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_factoid_search (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace, full_document_tsvector, last_rendered) AS (
SELECT DISTINCT ON (original_subject) lo.depth, factoid_id, subject,
copula, predicate, author, modified_time, compose_macro, protected,
original_subject, f.deleted, f.server, f.namespace, f.full_document_tsvector, f.last_rendered
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE NOT deleted
ORDER BY original_subject ASC, depth ASC, factoid_id DESC
)
SELECT ts_rank(full_document_tsvector, websearch_to_tsquery('factoid', ?)) AS rank, * FROM get_factoid_search WHERE ts_rank(full_document_tsvector, websearch_to_tsquery('factoid', ?)) > 0.01 ORDER BY 1 DESC, factoid_id DESC LIMIT 10
",
{ Slice => {} },
$namespace, $server,
$body, $body
);
if ($results and @$results) {
my $ret_string;
for (@$results) {
#i want a better string here, i'll probably go with just the subject, XXX TODO
$ret_string .= "[" . _fact_literal_format($_, $aliasserver, $aliasnamespace) . "]\n"
if ($_->{predicate} !~ /^\s*$/);
}
return $ret_string;
} else {
return "No matches.";
}
}
sub get_fact_namespace_chain ($self, $body, $name, $said) {
local $said->{channel} = $said->{channel};
if ($body) {
$said->{channel} = $body;
}
my ($server, $namespace) = $self->get_namespace($said);
print STDERR "XXX: $body $said->{channel} $server $namespace\n";
#XXX: need to also search contents of factoids TODO
my $results = $self->dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
)
SELECT depth, namespace, server FROM factoid_lookup_order_inner
", { Slice => {} }, $namespace, $server);
print STDERR "XXX: $body $said->{channel} $server $namespace\n";
print STDERR Dumper($results);
my $return = join ' -> ', map {sprintf "%d. <%s:%s>", $_->{depth}+1, $_->{server}||"*", $_->{namespace}||"##NULL"} $results->@*;
$return ||= "<$server:$namespace>"; # default namespace display
return $return;
}
sub get_fact_grep ($self, $body, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
my $results;
my $value_only = $body =~ s/\s*--val\s+//;
#XXX: need to also search contents of factoids TODO
$results = $self->dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject ~* ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT DISTINCT ON(original_subject) original_subject, predicate FROM get_latest_factoid WHERE NOT deleted ORDER BY original_subject ASC, depth ASC, factoid_id DESC",
{ Slice => {} },
$namespace, $server,
$body,
);
print STDERR "Got results: ".Dumper($results);
if ($results and @$results) {
my $ret_string = encode_json([map {$value_only ? $_->{predicate} : $_->{original_subject}} @$results]);
return $ret_string;
} else {
return "[]";
}
}
sub get_fact_oldsearch ($self, $body, $name, $said) {
my ($aliasserver, $aliasnamespace) = $self->get_alias_namespace($said);
my ($server, $namespace) = $self->get_namespace($said);
$body =~ s/^\s*for\s*//; #remove the for from searches
my $results;
if ($body =~ m|^\s*m?/(.*)/\s*$|) {
my $search = $1;
print STDERR "Got regex, $search\n";
#XXX: need to also search contents of factoids TODO
$results = $self->dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject ~* ? OR predicate ~* ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT DISTINCT ON(original_subject) * FROM get_latest_factoid WHERE NOT deleted ORDER BY original_subject ASC, depth ASC, factoid_id DESC",
{ Slice => {} },
$namespace, $server,
$search, $search,
);
} else {
print STDERR "No regex found, searching $body\n";
#XXX: need to also search contents of factoids TODO
$results = $self->dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject ILIKE ? OR predicate ILIKE ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT DISTINCT ON(original_subject) * FROM get_latest_factoid WHERE NOT deleted ORDER BY original_subject ASC, depth ASC, factoid_id DESC",
{ Slice => {} },
$namespace, $server,
"%$body%", "%$body%",
);
}
print STDERR "Got results: ".Dumper($results);
if ($results and @$results) {
my $ret_string;
for (@$results) {
#i want a better string here, i'll probably go with just the subject, XXX TODO
$ret_string .= "[" . _fact_literal_format($_, $aliasserver, $aliasnamespace) . "]\n"
if ($_->{predicate} !~ /^\s*$/);
}
return $ret_string;
} else {
return "No matches.";
}
}
sub get_fact ($self, $pm, $said, $subject, $name, $call_only) {
return $self->basic_get_fact($pm, $said, $subject, $name, $call_only);
}
sub _db_check_perm ($self, $subj, $said) {
my ($server, $namespace) = $self->get_namespace($said);
print STDERR "inside check perm\n";
my $isprot = $self->_db_get_protect($subj, $server, $namespace);
warn "Checking permissions of [$subj] for [$said->{name}]";
warn Dumper($said);
#always refuse to change factoids if not in one of my channels
return 0 if (!$said->{in_my_chan});
#if its not protected no need to check if they are op or root;
return 1 if (!$isprot);
if ($isprot && ($said->{by_root} || $said->{by_chan_op})) {
return 1;
}
#default case, $isprotect true; op or root isn't
return 0;
}
#get the status of the protection bit
sub _db_get_protect ($self, $subj, $server, $namespace) {
$subj = _clean_subject($subj);
my $dbh = $self->dbh;
my $prot = (
$dbh->selectrow_array("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT DISTINCT ON(lo.depth) lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject = ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT protected FROM get_latest_factoid WHERE NOT deleted ORDER BY depth ASC, factoid_id DESC LIMIT 1;
",
undef,
$namespace, $server,
$subj,
)
)[0];
return $prot;
}
sub _db_get_fact ($self, $subj, $func, $server, $namespace) {
# TODO write the recursive CTE for this
my $dbh = $self->dbh;
my $fact = $dbh->selectrow_hashref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_latest_factoid (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace) AS (
SELECT DISTINCT ON(lo.depth) lo.depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, f.deleted, f.server, f.namespace
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE original_subject = ?
ORDER BY depth ASC, factoid_id DESC
)
SELECT * FROM get_latest_factoid WHERE NOT deleted ORDER BY depth ASC, factoid_id DESC LIMIT 1;
",
undef,
$namespace, $server,
$subj,
);
if ($func && (!$fact->{compose_macro})) {
return undef;
} else {
return $fact;
}
}
sub basic_get_fact ($self, $pm, $said, $subject, $name, $call_only) {
my ($server, $namespace) = $self->get_namespace($said);
# open(my $fh, ">>/tmp/facts");
my ($fact, $key, $arg);
$key = _clean_subject($subject);
if (!$call_only) {
$fact = $self->_db_get_fact($key, 0, $server, $namespace);
}
# Attempt to determine if our subject matches a previously defined
# 'macro' or 'func' type factoid.
# I suspect it won't match two word function names now.
for my $variant (0, 1) {
if (!$fact) {
($key, $arg) = _clean_subject_func($subject, $variant);
$fact = $self->_db_get_fact($key, 1, $server, $namespace);
}
}
if ($fact->{predicate} =~ /\S/) {
if ($fact->{compose_macro}) {
my $plugin = $pm->get_plugin("compose", $said);
local $said->{macro_arg} = $arg;
local $said->{body} = $fact->{predicate};
local $said->{addressed} = 1; # Force addressed to circumvent restrictions? May not be needed!
open(my $fh, ">/tmp/wutwut");
print $fh Dumper($said, $plugin, $pm);
my $ret = $plugin->command($said, $pm);
use Data::Dumper;
print $fh Dumper({ key => $key, arg => $arg, fact => $fact, ret => $ret });
$self->set_last_rendered($fact, $ret);
$ret = "\x00$ret" if ($key eq "tell");
return $ret;
} else {
return "$fact->{predicate}";
}
} else {
if ($subject =~ /[\?\.\!]$/)
#check if some asshole decided to add a ? at the end of the factoid, if so remove it and recurse, this should only be able to recurse N times so it should be fine
{
my $newsubject = $subject;
$newsubject =~ s/[\?\.\!]$//;
return $self->basic_get_fact($pm, $said, $newsubject, $name, $call_only);
}
print STDERR "Got to here\n";
my $matches = $self->get_suggestions($key, $server, $namespace);
push @{ $said->{suggestion_matches} }, @$matches;
if (!$said->{nosuggest} && ($matches and @$matches) && (!$said->{backdressed})) {
return "No factoid found. Did you mean one of these: " . join " ", map "[$_]", @$matches;
} else {
return;
}
}
}
sub get_suggestions($self, $subject, $server, $namespace) {
my $dbh = $self->dbh;
print STDERR "Running search for $subject\n";
my $threshold = 0.2;
# TODO this should be using the trigram stuff once it's ready
my $rows = $dbh->selectall_arrayref("
WITH RECURSIVE factoid_lookup_order_inner (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT 0 AS depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, generated_server, generated_namespace
FROM factoid_config
WHERE namespace = ? AND server = ?
UNION ALL
SELECT p.depth+1 AS depth, m.namespace, m.server, m.alias_namespace, m.alias_server, m.parent_namespace, m.parent_server, m.recursive, m.generated_server, m.generated_namespace
FROM factoid_config m
INNER JOIN factoid_lookup_order_inner p
ON m.namespace = p.parent_namespace AND m.server = p.parent_server AND p.recursive
),
factoid_lookup_order (depth, namespace, server, alias_namespace, alias_server, parent_namespace, parent_server, recursive, gen_server, gen_namespace) AS (
SELECT * FROM factoid_lookup_order_inner
UNION ALL
SELECT 0, '', '', NULL, NULL, NULL, NULL, false, '', '' WHERE NOT EXISTS (table factoid_lookup_order_inner)
),
get_factoid_trigram (depth, factoid_id, subject, copula, predicate, author, modified_time, compose_macro, protected, original_subject, deleted, server, namespace, similarity) AS (
SELECT DISTINCT ON (lo.depth, original_subject) lo.depth, factoid_id, subject,
copula, predicate, author, modified_time, compose_macro, protected,
original_subject, f.deleted, f.server, f.namespace,
(difference(original_subject, ?) ::float + similarity(?, original_subject)) / greatest(length(?), length(original_subject))
FROM factoid f
INNER JOIN factoid_lookup_order lo
ON f.generated_server = lo.gen_server
AND f.generated_namespace = lo.gen_namespace
WHERE (difference(original_subject, ?) ::float + similarity(?, original_subject)) / greatest(length(?), length(original_subject)) > ?
ORDER BY depth ASC, original_subject ASC, factoid_id DESC
),
folddown AS (SELECT DISTINCT ON (similarity, original_subject) similarity, factoid_id, original_subject, predicate FROM get_factoid_trigram WHERE NOT deleted ORDER BY similarity DESC, original_subject, depth, factoid_id DESC)
SELECT * FROM folddown WHERE predicate ~ '\\S' AND predicate IS NOT NULL LIMIT 10
", undef,
$namespace, $server,
$subject, $subject, $subject, $subject, $subject, $subject, $threshold
);
print STDERR Dumper($rows);
return [grep {$_} map {$_->[2]} @$rows ];
}
sub set_last_rendered($self, $fact, $ret) {
my $factoid_id = $fact->{factoid_id};
my $dbh = $self->dbh;
$dbh->do("UPDATE factoid SET last_rendered = ? WHERE factoid_id = ?", undef,
$ret, $factoid_id
);
# TODO trigger FTS update?
}
no warnings 'void';
"Bot::BB3::Plugin::Factoids";
__DATA__
Learn or retrieve persistent factoids. "foo is bar" to store. "foo" to retrieve. try "forget foo" or "revisions foo" or "literal foo" or "revert $REV_ID" too. "macro foo is [echo bar]" or "func foo is [echo bar [arg]]" for compose macro factoids. The factoids/fact/call keyword is optional except in compose. Search <subject> to search for factoids that match.