From e19f38e57752cb3721b69b43a842a0e4b5d5c40f Mon Sep 17 00:00:00 2001 From: "ian%hixie.ch" Date: Mon, 25 Feb 2002 15:49:23 +0000 Subject: [PATCH] Implement Infobot functionality as a bot module. b=123692, r=timeless --- webtools/mozbot/BotModules/Infobot.bm | 762 +++++++++++++++++++++++++ webtools/mozbot/BotModules/Infobot.pl | 69 +++ webtools/mozbot/BotModules/Infobot.txt | 195 +++++++ 3 files changed, 1026 insertions(+) create mode 100644 webtools/mozbot/BotModules/Infobot.bm create mode 100755 webtools/mozbot/BotModules/Infobot.pl create mode 100644 webtools/mozbot/BotModules/Infobot.txt diff --git a/webtools/mozbot/BotModules/Infobot.bm b/webtools/mozbot/BotModules/Infobot.bm new file mode 100644 index 00000000000..aa7cf08f7cf --- /dev/null +++ b/webtools/mozbot/BotModules/Infobot.bm @@ -0,0 +1,762 @@ +# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*- +################################ +# Infobot Module # +################################ +# some of these ideas are stolen from infobot, of course. +# see www.infobot.org + +package BotModules::Infobot; +use vars qw(@ISA); +@ISA = qw(BotModules); +use AnyDBM_File; +use Fcntl; +1; + +# XXX "mozbot is a bot" fails (gets handled as a Tell of "is a bot" :-/) +# XXX "who is foo" responds "I don't know what is foo" (should respond "I don't know _who_ is foo") + +# it seems tie() works on scope and not on reference counting, so as +# soon as the thing it is tying goes out of scope (even if the variable +# in question still has active references) it loses its magic. +our $factoids = {'is' => {}, 'are' => {}}; +tie(%{$factoids->{'is'}}, 'AnyDBM_File', 'factoids-is', O_RDWR|O_CREAT, 0666); +tie(%{$factoids->{'are'}}, 'AnyDBM_File', 'factoids-are', O_RDWR|O_CREAT, 0666); + +sub Help { + my $self = shift; + my ($event) = @_; + return { + '' => 'Keeps track of factoids and returns them on request. '. + 'To set factoids, just tell me something in the form \'apple is a company\' or \'apples are fruit\'. '. + 'To find out about something, say \'apple?\' or \'what are apples\'. '. + 'To correct me, you can use any of: \'no, apple is a fruit\', \'apple =~ s/company/fruit/\', or \'apple is also a fruit\'. '. + 'To make me forget a factoid, \'forget apple\'. '. + 'You can use \'|\' to separate several alternative answers.', + 'who' => 'If a definition contains $who, then it will be replaced by the name of the person who asked the question.', + 'reply' => 'If a definition starts with then when responding the initial prefix will be skipped. '. + 'e.g., \'apples are mm, apples\' will mean that \'what are apples\' will get the response \'mm, apples\'.', + 'action' => 'If a definition starts with then when responding the definition will be used as an action. '. + 'e.g., \'apples are eats one\' will mean that \'what are apples\' will get the response \'* bot eats one\'.', + 'alias' => 'If a definition starts with then it will be treated as a symlink to whatever follows. '. + 'e.g., \'crab apples are apples\' and \'apples are fruit\' will mean that \'what are crab apples\' will get the response \'apples are fruit\'.', + 'status' => 'Reports on how many factoids are in the database.', + 'tell' => 'Make me tell someone something. e.g., \'tell pikachu what apples are\' or \'tell fred about me\'.', + 'literal' => 'To find out exactly what is stored for an entry apples, you would say to me: literal apples', + 'remember' => 'If you are having trouble making me remember something (for example \'well, foo is bar\' '. + 'getting treated as \'foo\' is \'bar\'), then you can prefix your statement with \'remember:\' '. + '(following the \'no,\' if you are changing an entry). For example, \'remember: well, foo is bar\'. '. + 'Note that \'well, foo?\' is treated as \'what is foo\' not is \'what is well, foo\', so this is not always useful.', + 'no' => 'To correct an entry, prefix your statement with \'no,\'. '. + 'For example, \'no, I am good\' to correct your entry from \'is bad\' to \'is good\'. :-)', + }; +} + +# RegisterConfig - Called when initialised, should call registerVariables +sub RegisterConfig { + my $self = shift; + $self->SUPER::RegisterConfig(@_); + $self->registerVariables( + # [ name, save?, settable? ] + ['autoLearn', 1, 1, ['*']], # in the auto* variables, '*' means 'all channels' + ['autoHelp', 1, 1, ['*']], + ['autoEdit', 1, 1, []], + ['neverLearn', 1, 1, []], # the never* variables override the auto* variables + ['neverHelp', 1, 1, []], + ['neverEdit', 1, 1, []], + ['autoIgnore', 1, 1, []], # list of nicks for which to always turn off auto* + ['teachers', 1, 1, []], # list of users who may teach, leave blank to allow anyone to teach + ['factoidPositions', 0, 0, {'is' => {}, 'are' => {}}], + ['friendBots', 1, 1, []], + ['researchNotes', 0, 0, {}], + ['pruneDelay', 1, 1, 120], # how frequently to look through the research notes and remove expired items + ['queryTimeToLive', 1, 1, 600], # queries can be remembered up to ten minutes by default + ['dunnoTimeToLive', 1, 1, 604800], # DUNNO queries can be remembered up to a week by default + ['noIdeaDelay', 1, 1, 2], # how long to wait before admitting lack of knowledge + ['questions', 0, 0, 0], # how many questions there have been since the last load + ['edits', 0, 0, 0], # how many edits (learning, editing, forgetting) there have been since the last load + ['interbots', 0, 0, 0], # how many times we have spoken with other bots + ['maxInChannel', 1, 1, 200], # beyond this answers are /msged + ); +} + +# Schedule - called when bot connects to a server, to install any schedulers +# use $self->schedule($event, $delay, $times, $data) +# where $times is 1 for a single event, -1 for recurring events, +# and a positive number for an event that occurs that many times. +sub Schedule { + my $self = shift; + my ($event) = @_; + $self->schedule($event, \$self->{'pruneDelay'}, -1, 'pruneInfobot'); + $self->SUPER::Schedule($event); +} + +sub unload { + my $self = shift; + $self->SUPER::unload(@_); + # just to make sure... + untie(%{$factoids->{'is'}}); + untie(%{$factoids->{'are'}}); +} + +sub Told { + my $self = shift; + my ($event, $message) = @_; + if ($message =~ /^\s*status[?\s]*$/osi) { + my $sum = $self->countFactoids(); + my $questions = $self->{'questions'} == 1 ? "$self->{'questions'} question" : "$self->{'questions'} questions"; + my $edits = $self->{'edits'} == 1 ? "$self->{'edits'} edit" : "$self->{'edits'} edits"; + my $interbots = $self->{'interbots'} == 1 ? "$self->{'interbots'} time" : "$self->{'interbots'} times"; + my $friends = @{$self->{'friendBots'}} == 1 ? (scalar(@{$self->{'friendBots'}}).' bot friend') : (scalar(@{$self->{'friendBots'}}).' bot friends'); + $self->targettedSay($event, "I have $sum factoids in my database and $friends to help me answer questions. ". + "Since the last reload, I've been asked $questions, performed $edits, and spoken with other bots $interbots.", 1); + } elsif ($event->{'channel'} eq '' and $message =~ /^:INFOBOT:DUNNO <(\S+)> (.*)$/) { + $self->ReceivedDunno($event, $1, $2); + } elsif ($event->{'channel'} eq '' and $message =~ /^:INFOBOT:QUERY <(\S+)> (.*)$/) { + $self->ReceivedQuery($event, $2, $1); + } elsif ($event->{'channel'} eq '' and $message =~ /^:INFOBOT:REPLY <(\S+)> (.+?) =(is|are)?=> (.*)$/) { + $self->ReceivedReply($event, $3, $2, $1, $4); + } elsif ($message =~ /^\s*literal\s+(.+?)\s*$/) { + $self->Literal($event, $1); + } elsif (not $self->DoFactoidCheck($event, $message, 1)) { + return $self->SUPER::Told(@_); + } + return 0; # we've dealt with it, no need to do anything else. +} + +sub Baffled { + my $self = shift; + my ($event, $message) = @_; + if (not $self->DoFactoidCheck($event, $message, 2)) { + return $self->SUPER::Heard(@_); + } + return 0; # we've dealt with it, no need to do anything else. +} + +sub Heard { + my $self = shift; + my ($event, $message) = @_; + if (not $self->DoFactoidCheck($event, $message, 0)) { + return $self->SUPER::Heard(@_); + } + return 0; # we've dealt with it, no need to do anything else. +} + +sub DoFactoidCheck { + my $self = shift; + my ($event, $message, $direct) = @_; + # $direct is one of: 0 = heard, 1 = told, 2 = baffled + + my $shortMessage; + if ($message =~ /^\s* (?:(?:well|and|or|yes|[uh]+m*|o+[oh]*[k]+(?:a+y+)?|still|well|so|[ah]+|[oh]+)[:,.!?\s]+|)* + (?:(?:geez?|boy|du+des?|golly|gosh|wow|whee|wo+ho+)?[:,.!\s]+|)* + (?:(?:heya?|hello|hi)(?:\s+there)?(?:\s+peoples?|\s+kids?|\s+folks?)?[:,!.?\s]+)* + (?:(?:geez?|boy|du+des?|golly|gosh|wow|whee|wo+ho+)?[:,.!\s]+|)* + (?:(?:(?:stupid\s+)?q(?:uestion)?|basically)[:,.!\s]+)* + (?:(?:does\s+)?(?:any|ne)\s*(?:1|one|body)\s+know[,\s]+|)? + (.*) + \s*$/osix) { + $shortMessage = $1; + } + + if ($message =~ /^\s*tell\s+(\S+)\s+about\s+me(?:[,\s]+please)?[\s!?.]*$/osi) { + $self->GiveFactoid($event, + undef, # database + $event->{'from'}, # what + $direct, + $1); # who + } elsif ($message =~ /^\s*tell\s+(\S+)\s+about\s+(.+?)(?:[,\s]+please)?[\s!?.]*$/osi) { + $self->GiveFactoid($event, + undef, # database + $2, # what + $direct, + $1); # who + } elsif ($message =~ /^\s*tell\s+(\S+)\s+(?:what|who|where)\s+(?:am\s+I|I\s+am)(?:[,\s]+please)?[\s!?.]*$/osi) { + $self->GiveFactoid($event, + 'is', # database + $event->{'from'}, # what + $direct, + $1); # who + } elsif ($message =~ /^\s*tell\s+(\S+)\s+(?:what|who|where)\s+(is|are)\s+(.+?)(?:[,\s]+please)?[\s!?.]*$/osi) { + $self->GiveFactoid($event, + lc($2), # database + $3, # what + $direct, + $1); # who + } elsif ($message =~ /^\s*tell\s+(\S+)\s+(?:what|who|where)\s+(.+?)\s+(is|are)(?:[,\s]+please)?[\s!?.]*$/osi) { + $self->GiveFactoid($event, + lc($3), # database + $2, # what + $direct, + $1); # who + } elsif ($message =~ /^\s*(.+?)\s*=~\s*s?\/(.+?)\/(.*?)\/(i)?(g)?(i)?\s*$/osi) { + $self->EditFactoid($event, + $1, # subject + $2, # first part to remove + $3, # second part to remove + defined($5), # global? + defined($4) || defined($6), # case insensitive? + $direct); + } elsif ($message =~ /^\s*forget\s+(?:about\s+)?me\s*$/osi) { + $self->ForgetFactoid($event, $event->{'from'}, $direct); + } elsif ($message =~ /^\s*forget\s+(?:about\s+)?(.+?)\s*$/osi) { + $self->ForgetFactoid($event, $1, $direct); + } elsif ($shortMessage =~ /^(?:what|where|who) \s+ (is|are) \s+ (.+?) [?\s]* $/osix) { + $self->GiveFactoid($event, + lc($1), # is/are (optional) + $2, # subject + $direct); + } elsif ($shortMessage =~ /^(?:(?:where|how)\s+can\s+(?:i|one|s?he|we)\s+(?:find|learn|read)(?:\s+about)? | how\s+about | what\'?s) \s+ (.+?) [?\s]* $/osix) { + $self->GiveFactoid($event, + undef, # is/are (optional) + $1, # subject + $direct); + } elsif ($shortMessage =~ /^(?:what|where|who)\s+ (.+?) (is|are) [?\s]* $/osix) { + $self->GiveFactoid($event, + lc($2), # is/are (optional) + $1, # subject + $direct); + } elsif ($shortMessage =~ /^(?:what|where|who) \s+ (?:am\s+I|I\s+am) [?\s]* $/osix) { + $self->GiveFactoid($event, + 'is', # am => is + $event->{'from'}, # subject + $direct); + } elsif ($shortMessage =~ /^(no\s*, (\s*$event->{'nick'}\s*,)? \s+)? (?:remember\s*:\s+)? (.+?) \s+ (is|are) \s+ (also\s+)? (.+?) $/six) { + # the "remember:" prefix can be used to delimit the start of the actual content, if necessary. + $self->SetFactoid($event, + defined($1), # replace existing answer? + $3, # subject + lc($4), # is/are + defined($5), # add to existing answer? + $6, # object + ($direct or defined($2))); + } elsif ($shortMessage =~ /^(no\s*, (\s*$event->{'nick'}\s*,)? \s+)? (?:remember\s*:\s+)? I \s+ am \s+ (also\s+)? (.+?) $/osix) { + # the "remember:" prefix can be used to delimit the start of the actual content, if necessary. + $self->SetFactoid($event, + defined($1), # replace existing answer? + $event->{'from'}, # subject + 'is', # I am = Foo is + defined($2), # add to existing answer? + $3, # object + $direct); + } elsif ((not $direct or $direct == 2) and $shortMessage =~ /^(.+?)\s+(is|are)[?\s]*\?[?\s]*$/osi) { + $self->GiveFactoid($event, + lc($2), # is/are (optional) + $1, # subject + $direct); + } elsif ((not $direct or $direct == 2) and $shortMessage =~ /^(.+?)[?\s]*\?[?\s]*$/osi) { + $self->GiveFactoid($event, + undef, # is/are (optional) + $1, # subject + $direct); + } else { + return 0; + } + return 1; +} + +sub SetFactoid { + my $self = shift; + my($event, $replace, $subject, $database, $add, $object, $direct, $fromBot) = @_; + if ($direct or $self->allowed($event, 'Learn')) { + + teacher: if (@{$self->{'teachers'}}) { + foreach my $user (@{$self->{'teachers'}}) { + if ($user eq $event->{'userName'}) { + last teacher; + } + } + return 0; + } + + # update the database + if (not $replace) { + $subject = $self->CanonicalizeFactoid($database, $subject); + } else { + my $oldSubject = $self->CanonicalizeFactoid($database, $subject); + if (defined($factoids->{$database}->{$oldSubject})) { + delete($factoids->{$database}->{$oldSubject}); + } + } + if ($replace or not defined($factoids->{$database}->{$subject})) { + $self->debug("Learning that $subject $database '$object'."); + $factoids->{$database}->{$subject} = $object; + } elsif (not $add) { + my @what = split(/\|/o, $factoids->{$database}->{$subject}); + local $" = '\' or \''; + if (not defined($fromBot)) { + if (@what == 1 and $what[0] eq $object) { + $self->targettedSay($event, 'Yep, that\'s what I thought. Thanks for confirming it.', $direct); + } else { + # XXX "that's one of the alternatives, sure..." + $self->targettedSay($event, "But $subject $database '@what'...", $direct); + } + } + return 0; # failed to update database + } else { + $self->debug("Learning that $subject $database also '$object'."); + $factoids->{$database}->{$subject} .= "|$object"; + } + if (not defined($fromBot)) { + $self->targettedSay($event, 'ok', $direct); + } + if (defined($self->{'researchNotes'}->{lc($subject)})) { + my @queue = @{$self->{'researchNotes'}->{lc($subject)}}; + foreach my $entry (@queue) { + my($eventE, $typeE, $databaseE, $subjectE, $targetE, $directE, $visitedAliasesE, $timeE) = @$entry; + if ($typeE eq 'QUERY') { + if ((defined($targetE) and $event->{'from'} ne $targetE) or + ($event->{'from'} ne $eventE->{'from'} and + ($event->{'channel'} eq '' or $event->{'channel'} ne $eventE->{'channel'}))) { + my($how, $what, $propagated) = $self->GetFactoid($eventE, $databaseE, $subjectE, + $targetE, $directE, $visitedAliasesE, $event->{'from'}); + if (defined($how)) { + if (defined($targetE)) { + $self->debug("I now know what '$subject' $database, so telling $targetE, since $eventE->{'from'} told me to."); + } else { + $self->debug("I now know what '$subject' $database, so telling $eventE->{'from'} who wanted to know."); + } + $self->factoidSay($eventE, $how, $what, $directE, $targetE); + $entry->[1] = 'OLD'; + } else { + # either $propagated, or database doesn't match requested database, or internal error + $self->debug("I now know what '$subject' $database, but for some reason that ". + "didn't help me help $eventE->{'from'} who needed to know what '$subjectE' $databaseE."); + } + } + } elsif ($typeE eq 'DUNNO') { + my $who = defined($targetE) ? $targetE : $eventE->{'from'}; + $self->directSay($eventE, ":INFOBOT:REPLY <$who> $subject =$database=> $factoids->{$database}->{$subject}"); + $entry->[1] = 'OLD'; + } + } + } + $self->{'edits'}++; + return 1; + } else { + return 0; + } +} + +sub GiveFactoid { + my $self = shift; + my($event, $database, $subject, $direct, $target) = @_; + if ($direct or $self->allowed($event, 'Help')) { + if ($target eq $event->{'nick'}) { + $self->targettedSay($event, 'Oh, yeah, great idea, get me to talk to myself.', $direct); + } else { + if (lc($subject) eq 'you') { + # first, skip some words that are handled by other commonly-used modules + # in particular, 'who are you' is handled by Greeting.bm + return; + } + $self->{'questions'}++; + my($how, $what, $propagated) = $self->GetFactoid($event, $database, $subject, $target, $direct); + if (not defined($how)) { + $self->scheduleNoIdea($event, $database, $subject, $direct, $propagated); + } else { + $self->debug("Telling $event->{'from'} about $subject."); + $self->factoidSay($event, $how, $what, $direct, $target); + } + } + } +} + +sub Literal { + my $self = shift; + my($event, $subject) = @_; + my $is = $self->CanonicalizeFactoid('is', $subject); + my $are = $self->CanonicalizeFactoid('are', $subject); + if (defined($is) or defined($are)) { + local $" = '\' or \''; + if (defined($factoids->{'is'}->{$is})) { + my @what = split(/\|/o, $factoids->{'is'}->{$is}); + $self->targettedSay($event, "$is is '@what'.", 1); + } + if (defined($factoids->{'are'}->{$are})) { + my @what = split(/\|/o, $factoids->{'are'}->{$is}); + $self->targettedSay($event, "$are are '@what'.", 1); + } + } else { + $self->targettedSay($event, "I have no record of anything called '$subject'.", 1); + } +} + +sub scheduleNoIdea { + my $self = shift; + my($event, $database, $subject, $direct, $propagated) = @_; + if (ref($propagated)) { + $self->schedule($event, \$self->{'noIdeaDelay'}, 1, 'noIdea', $database, $subject, $direct, $propagated); + } else { + $self->noIdea($event, $database, $subject, $direct); + } +} + +sub GetFactoid { + my $self = shift; + my($event, $originalDatabase, $subject, $target, $direct, $visitedAliases, $friend) = @_; + if (not defined($visitedAliases)) { + $visitedAliases = {}; + } + my $database; + ($database, $subject) = $self->FindFactoid($originalDatabase, $subject); + if (defined($factoids->{$database}->{$subject})) { + my @alternatives = split(/\|/o, $factoids->{$database}->{$subject}); + my $answer; + if (@alternatives) { + if (not defined($self->{'factoidPositions'}->{$database}->{$subject}) + or $self->{'factoidPositions'}->{$database}->{$subject} >= scalar(@alternatives)) { + $self->{'factoidPositions'}->{$database}->{$subject} = 0; + } + $answer = @alternatives[$self->{'factoidPositions'}->{$database}->{$subject}]; + $self->{'factoidPositions'}->{$database}->{$subject}++; + } else { + $answer = @alternatives[0]; + } + my $who = defined($target) ? $target : $event->{'from'}; + $answer =~ s/\$who/$who/go; + if ($answer =~ /^(.*)$/o) { + if ($visitedAliases->{$1}) { + return ('msg', "see $subject", 0); + } else { + $visitedAliases->{$subject}++; + my($how, $what, $propagated) = $self->GetFactoid($event, undef, $1, $target, $direct, $visitedAliases); + if (not defined($how)) { + return ('msg', "see $1", $propagated); + } else { + return ($how, $what, $propagated); + } + } + } elsif ($answer =~ /^/o) { + $answer =~ s/^\s*//o; + return ('me', $answer, 0); + } else { + if ($answer =~ /^/o) { + $answer =~ s/^\s*//o; + } else { + if (lc($who) eq lc($subject)) { + $answer = "you are $answer"; + } else { + $answer = "$subject $database $answer"; + } + if (defined($friend)) { + $answer = "$friend knew: $answer"; + } + } + return ('msg', $answer, 0); + } + } else { + # we have no idea what this is + return (undef, undef, $self->Research($event, $originalDatabase, $subject, $target, $direct, $visitedAliases)); + } +} + +sub CanonicalizeFactoid { + my $self = shift; + my($database, $subject) = @_; + if (not defined($factoids->{$database}->{$subject})) { + while (my $key = each %{$factoids->{$database}}) { + if (lc($key) eq lc($subject)) { + $subject = $key; + # can't return or 'each' iterator won't be reset XXX + } + } + } + return $subject; +} + +sub FindFactoid { + my $self = shift; + my($database, $subject) = @_; + if (not defined($database)) { + $database = 'is'; + $subject = $self->CanonicalizeFactoid('is', $subject); + if (not defined($factoids->{'is'}->{$subject})) { + $subject = $self->CanonicalizeFactoid('are', $subject); + if (defined($factoids->{'are'}->{$subject})) { + $database = 'are'; + } + } + } else { + $subject = $self->CanonicalizeFactoid($database, $subject); + } + return ($database, $subject); +} + +sub EditFactoid { + my $self = shift; + my($event, $subject, $search, $replace, $global, $caseInsensitive, $direct) = @_; + if ($direct or $self->allowed($event, 'Edit')) { + my $database; + ($database, $subject) = $self->FindFactoid($database, $subject); + if (not defined($factoids->{$database}->{$subject})) { + $self->targettedSay($event, "Er, I don't know about this $subject thingy...", $direct); + return; + } + $self->debug("Editing the $subject entry."); + my @output; + foreach my $factoid (split(/\|/o, $factoids->{$database}->{$subject})) { + $search = $self->sanitizeRegexp($search); + if ($global and $caseInsensitive) { + $factoid =~ s/$search/$replace/gi; + } elsif ($global) { + $factoid =~ s/$search/$replace/g; + } elsif ($caseInsensitive) { + $factoid =~ s/$search/$replace/i; + } else { + $factoid =~ s/$search/$replace/; + } + push(@output, $factoid); + } + $factoids->{$database}->{$subject} = join('|', @output); + $self->targettedSay($event, 'ok', $direct); + $self->{'edits'}++; + } +} + +sub ForgetFactoid { + my $self = shift; + my($event, $subject, $direct) = @_; + if ($direct or $self->allowed($event, 'Edit')) { + my $count = 0; + my $database; + foreach my $db ('is', 'are') { + ($database, $subject) = $self->FindFactoid($db, $subject); + if (defined($factoids->{$database}->{$subject})) { + delete($factoids->{$database}->{$subject}); + $count++; + } + } + if ($count) { + $self->targettedSay($event, "I've forgotten what I knew about '$subject'.", $direct); + $self->{'edits'}++; + } else { + $self->targettedSay($event, "I never knew anything about '$subject' in the first place!", $direct); + } + } +} + +# interbot communications +sub Research { + my $self = shift; + my($event, $database, $subject, $target, $direct, $visitedAliases) = @_; + if (not @{$self->{'friendBots'}}) { + # no bots to ask, bail out + return 0; + } + # now check that we need to ask the bots about it: + my $asked = 0; + if (not defined($self->{'researchNotes'}->{$subject})) { + $self->{'researchNotes'}->{$subject} = []; + } else { + entry: foreach my $entry (@{$self->{'researchNotes'}->{lc($subject)}}) { + my($eventE, $typeE, $databaseE, $subjectE, $targetE, $directE, $visitedAliasesE, $timeE) = @$entry; + if ($typeE eq 'QUERY') { + $asked++; # at least one bot was already asked quite recently + if ((defined($targetE) and lc($targetE) eq lc($targetE)) or + (not defined($targetE) and lc($event->{'from'}) eq lc($eventE->{'from'}))) { + # already queued + return 1; + } + } + } + } + # remember to tell these people about $subject if we ever find out about it: + my $entry = [$event, 'QUERY', $database, $subject, $target, $direct, $visitedAliases, time()]; + push(@{$self->{'researchNotes'}->{lc($subject)}}, $entry); + my $who = defined($target) ? $target : $event->{'from'}; + if (not $asked) { + # not yet asked, so ask each bot about $subject + foreach my $bot (@{$self->{'friendBots'}}) { + local $event->{'from'} = $bot; + $self->directSay($event, ":INFOBOT:QUERY <$who> $subject"); + } + $self->{'interbots'}++; + return $entry; # return reference to entry so that we can check if it has been replied or not + } else { + return $asked; + } +} + +sub ReceivedReply { + my $self = shift; + my($event, $database, $subject, $target, $object) = @_; + $self->{'interbots'}++; + if (not $self->SetFactoid($event, 0, $subject, $database, 0, $object, 1, 1) and + defined($self->{'researchNotes'}->{lc($subject)})) { + # we didn't believe $event->{'from'}, but we might as well + # tell any users that were wondering. + foreach my $entry (@{$self->{'researchNotes'}->{lc($subject)}}) { + my($eventE, $typeE, $databaseE, $subjectE, $targetE, $directE, $visitedAliasesE, $timeE) = @$entry; + if ($typeE eq 'QUERY') { + $self->factoidSay($eventE, 'msg', "According to $event->{'from'}, $subject $database '$object'.", $directE, $targetE); + } elsif ($typeE eq 'DUNNO') { + my $who = defined($targetE) ? $targetE : $eventE->{'from'}; + $self->directSay($eventE, ":INFOBOT:REPLY <$who> $subject =$database=> $object"); + } + $entry->[1] = 'OLD'; + } + } +} + +sub ReceivedQuery { + my $self = shift; + my($event, $subject, $target) = @_; + $self->{'interbots'}++; + if (not $self->tellBot($event, $subject, $target)) { + # in the spirit of embrace-and-extend, we're going to say that + # :INFOBOT:DUNNO means "I don't know, but if you ever find + # out, please tell me". + $self->directSay($event, ":INFOBOT:DUNNO <$event->{'nick'}> $subject"); + } +} + +sub ReceivedDunno { + my $self = shift; + my($event, $target, $subject) = @_; + $self->{'interbots'}++; + if (not $self->tellBot($event, $subject, $target)) { + # store the request + push(@{$self->{'researchNotes'}->{lc($subject)}}, [$event, 'DUNNO', undef, $1, $target, 0, {}, time()]); + } +} + +sub tellBot { + my $self = shift; + my($event, $subject, $target) = @_; + my $count = 0; + my $database; + foreach my $db ('is', 'are') { + ($database, $subject) = $self->FindFactoid($db, $subject); + if (defined($factoids->{$database}->{$subject})) { + $self->directSay($event, ":INFOBOT:REPLY <$target> $subject =$database=> $factoids->{$database}->{$subject}"); + $count++; + } + } + return $count; +} + +sub Scheduled { + my $self = shift; + my ($event, @data) = @_; + if ($data[0] eq 'pruneInfobot') { + my $now = time(); + foreach my $key (keys %{$self->{'researchNotes'}}) { + my @new; + foreach my $entry (@{$self->{'researchNotes'}->{$key}}) { + my($eventE, $typeE, $databaseE, $subjectE, $targetE, $directE, $visitedAliasesE, $timeE) = @$entry; + if (($typeE eq 'QUERY' and ($now - $timeE) < $self->{'queryTimeToLive'}) or + ($typeE eq 'DUNNO' and ($now - $timeE) < $self->{'dunnoTimeToLive'})) { + push(@new, $entry); + } + } + if (@new) { + $self->{'researchNotes'}->{$key} = \@new; + } else { + delete($self->{'researchNotes'}->{$key}); + } + } + } elsif ($data[0] eq 'noIdea') { + my(undef, $database, $subject, $direct, $propagated) = @data; + my($eventE, $typeE, $databaseE, $subjectE, $targetE, $directE, $visitedAliasesE, $timeE) = @$propagated; + # in theory, $eventE = $event, $databaseE = $database, + # $subjectE = $subject, $targetE depends on if this was + # triggered by a tell, $directE = $direct, $visitedAliasesE is + # opaque, and $timeE is opaque. + if ($typeE ne 'OLD') { + $self->noIdea($event, $database, $subject, $direct); + } + } else { + $self->SUPER::Scheduled($event, @data); + } +} + + +# internal helper routines + +sub factoidSay { + my $self = shift; + my($event, $how, $what, $direct, $target) = @_; + if (defined($target)) { + $self->targettedSay($event, "told $target", 1); + my $helper = $event->{'from'}; + local $event->{'from'} = $target; + if ($how eq 'me') { + $self->directEmote($event, $what); + } else { + if (length($what)) { + $self->directSay($event, "$helper wanted you to know: $what"); + } + } + } elsif ($how eq 'me') { + $self->emote($event, $what); + } else { + if ($event->{'channel'} eq '' or length($what) < $self->{'maxInChannel'}) { + $self->targettedSay($event, $what, 1); + } else { + if ($direct) { + $self->targettedSay($event, substr($what, 0, $self->{'maxInChannel'}) . '... (rest /msged)' , 1); + $self->directSay($event, $what); + } else { + $self->targettedSay($event, substr($what, 0, $self->{'maxInChannel'}) . '... (there is more; ask me in a /msg)' , 1); + } + } + } +} + +sub targettedSay { + my $self = shift; + my($event, $message, $direct) = @_; + if ($direct and length($message)) { + if ($event->{'channel'} ne '') { + $message = "$event->{'from'}: $message"; + } + $self->say($event, $message); + } +} + +sub countFactoids { + my $self = shift; + # don't want to use keys() as that would load the whole database index into memory. + my $sum = 0; + while (my $factoid = each %{$factoids->{'is'}}) { $sum++; } + while (my $factoid = each %{$factoids->{'are'}}) { $sum++; } + return $sum; +} + +sub allowed { + my $self = shift; + my($event, $type) = @_; + if ($event->{'channel'} ne '') { + foreach my $user (@{$self->{'autoIgnore'}}) { + if ($user eq $event->{'from'}) { + return 0; + } + } + foreach my $channel (@{$self->{"never$type"}}) { + if ($channel eq $event->{'channel'} or + $channel eq '*') { + return 0; + } + } + foreach my $channel (@{$self->{"auto$type"}}) { + if ($channel eq $event->{'channel'} or + $channel eq '*') { + return 1; + } + } + } + return 0; +} + +sub noIdea { + my $self = shift; + my($event, $database, $subject, $direct) = @_; + if (lc($subject) eq lc($event->{'from'})) { + $self->targettedSay($event, "Sorry, I've no idea who you are.", $direct); + } else { + if (not defined($database)) { + $database = 'might be'; + } + $self->targettedSay($event, "Sorry, I've no idea what '$subject' $database.", $direct); + } +} diff --git a/webtools/mozbot/BotModules/Infobot.pl b/webtools/mozbot/BotModules/Infobot.pl new file mode 100755 index 00000000000..cf2822b5078 --- /dev/null +++ b/webtools/mozbot/BotModules/Infobot.pl @@ -0,0 +1,69 @@ +#!/usr/bin/perl -w +###################################### +# Infobot Factoid Import/Export Tool # +###################################### + +use strict; +use AnyDBM_File; +use Fcntl; + +if (not @ARGV == 2) { + &use(); +} else { + my $command = shift @ARGV; + my $filename = shift @ARGV; + if ($command eq '-d') { + &dump($filename); + } elsif ($command eq '-i') { + &import($filename); + } else { + &use(); + } +} + +sub use { + print "\n"; + print " usage: $0 -d dbname\n"; + print " prints out an ascii flat file of the database listed.\n"; + print " dbname should be the basename of the db, e.g.\n"; + print " $0 -d ../factoids-is > is.fact\n"; + print " $0 -d ../factoids-are > are.fact\n"; + print "\n"; + print " $0 -i dbname\n"; + print " prints out an ascii flat file of the database listed.\n"; + print " dbname should be the basename of the db, e.g.\n"; + print " $0 -i ../factoids-is < chemicals.fact\n"; + print " $0 -i ../factoids-is < is.fact\n"; + print " $0 -i ../factoids-are < are.fact\n"; + print "\n"; + exit(1); +} + +sub dump { + my %db; + tie(%db, 'AnyDBM_File', shift, O_RDONLY, 0666); + while (my ($key, $val) = each %db) { + chomp $val; + print "$key => $val\n"; + } +} + +sub import { + my %db; + tie(%db, 'AnyDBM_File', shift, O_WRONLY|O_CREAT, 0666); + while () { + chomp; + unless (m/\s*(.+?)\s+=(?:is=|are=)?>\s+(.+?)\s*$/o) { + m/\s*(.+?)\s+(?:is|are)?\s+(.+?)\s*$/o; + } + if (length($1) and length($2)) { + if (defined($db{$1})) { + if (not $db{$1} =~ m/^(|.*\|)\Q$2\E(|.*\|)$/s) { + $db{$1} .= "|$2"; + } + } else { + $db{$1} = $2; + } + } + } +} diff --git a/webtools/mozbot/BotModules/Infobot.txt b/webtools/mozbot/BotModules/Infobot.txt new file mode 100644 index 00000000000..bd7ce998407 --- /dev/null +++ b/webtools/mozbot/BotModules/Infobot.txt @@ -0,0 +1,195 @@ +The Infobot Protocol +==================== + +Reverse engineered from infobot 0.45.3 by Ian Hickson. + + +QUERY +----- + +If a bot is asked something by a user and does not know the answer, it +may send queries to all the bots it knows. Queries must be in private +messages and should have the following form: + + :INFOBOT:QUERY subject + +...where "target" is the name of the user who sent the query in the +first place, and "subject" is the question that was asked. + +In reality, "target" may be any string of non-whitespace character, so +it could be used as an internal ID. + +A bot receiving a QUERY message must not try to contact the user given +by "target" (that string should be treated as opaque) and must not +make any assumptions about the "subject" string (it could contain +*anything*, including high bit characters and the works). + +It is an error for the "subject" string to contain either "=is=>" or +"=are=>". Receiving bots may ignore this error, however. + +Bot authors should carefully consider the potential for cascades +before writing bots that chain QUERY messages. (As in, send out QUERY +messages if they are unable to respond to a QUERY message themselves). +In general, this is not a recommended behaviour. + +Bot authors are urged to write protection into their bots to avoid +being affected by poorly written bots that cause cascades. + + +REPLY +----- + +Upon receiving a QUERY message, a bot may, if it has information on +"subject", opt to send a private message back to the originating bot +in the form of a REPLY message. Bots must not send unsolicited REPLY +messages. The form of the REPLY message is: + + :INFOBOT:REPLY subject =database=> object + +...where "target" is the string of the same name from the original +QUERY message, "subject" is the second string from the original QUERY +message, "database" is one of "is" or "are" depending on the whether +"subject" is determined to be singular or plural respectively, and +"object" is the string that should be assumed to be the answer to +"subject". The string may contain special formatting codes, these are +described below. + +Upon receiving a REPLY message, bots should first check that they are +expecting one. If they are, the user identified by the "target" string +should be contacted and given the information represented by the +"object" string. (Remember that the "target" string need not actually +be the nick of the original user; it could be an internal key that +indirectly identifies a user.) + +Bots should carefully check the integrity and authenticity of the +"target" string, and must check that "database" is one of "is" or +"are". The "subject" string ends at the first occurance of either +"=is=>" or "=are=>". It is *not* an error for the "object" string to +contain either of those substrings. + +Bots may opt to store the information given by a REPLY request so that +future questions may be answered without depending on other bots. + +It is suggested that bots credit which bot actually knew the +information when reporting back to the user. + + +DUNNO +----- + +(This is not part of the original infobot protocol. And is, as of +2002-02-05, only supported by the mozbot2 Infobot module.) + +Upon receiving a QUERY message, a bot may, if it has no information on +the "subject" in question, reply with a DUNNO message. This message +has basically the same form as the QUERY message: + + :INFOBOT:DUNNO subject + +The DUNNO message indicates that the bot is not aware of the answer to +the question, but would like to be informed of the answer, should the +first bot ever find out about it. The "target" string should, as with +the QUERY string, be considered opaque. + +Upon receiving a DUNNO message, there are several possible responses. +If the bot is aware of the answer to "subject", then it should treat +the DUNNO message as if it was a QUERY message (typically resulting in +a REPLY message). This can occur if, for example, another bot has sent +a REPLY to the original QUERY before this bot has had the chance to +send the DUNNO message. + +If the first bot still doesn't know the answer, however, it may store +the DUNNO request internally. If, at a future time, the bot is +informed (either directly by a user or through a REPLY message) about +the answer to "subject", then it may send a REPLY message to the bot +that sent the DUNNO request, informing the bot of the value it learnt. + + +SPECIAL STRINGS +--------------- + +The "object" string in the REPLY message may contain several special +flags. + + $who + If the string contains the string "$who" then, when the string is + given to the user, it should be replaced by the name of the user. + + | + Multiple alternative replies may be encoded in one reply, those + should be separated by a vertical bar. + + + If the string is prefixed by "" then the string should not + be prefixed by "subject is" or "subject are" as usual. + + + The string should be returned via a CTCP ACTION. + + + The string should be taken as the name of another entry to look up. + + +EXAMPLES +-------- + +In these examples, A, B and C are bots, and x, y and z are people. + +The first example shows a simple case of one bots asking two other +bots for help, one of which gives a reply and the other of which says +it has no idea. + + +-------- originator of private message + | + | +--- target of private message + | | + V V + z -> A: what is foo? + A -> z: I have no idea. + A -> B: :INFOBOT:QUERY foo + A -> C: :INFOBOT:QUERY foo + B -> A: :INFOBOT:REPLY foo =is=> bar + C -> A: :INFOBOT:DUNNO foo + A -> x: B knew: foo is bar + A -> C: :INFOBOT:REPLY foo =is=> bar + +Note how the DUNNO in this case comes after the REPLY and thus is +immediately answered. + +The next example uses . One bot knows the answer to the +question as an alias to another word, but when the original bot asks +about _that_ word, it is the second bot that can help. + + z -> A: what is foo? + A -> z: I have no idea. + A -> B: :INFOBOT:QUERY foo + A -> C: :INFOBOT:QUERY foo + B -> A: :INFOBOT:REPLY foo =is=> bar + C -> A: :INFOBOT:DUNNO foo + A -> B: :INFOBOT:QUERY bar + A -> C: :INFOBOT:QUERY bar + A -> C: :INFOBOT:REPLY foo =is=> bar + B -> A: :INFOBOT:DUNNO bar + C -> A: :INFOBOT:REPLY bar =is=> baz + A -> z: C knew: bar is baz + A -> B: :INFOBOT:REPLY bar =is=> baz + +Note how the credit actually goes to the second bot. A better bot +might remember all the bots involved and credit all of them. A better +bot might also remember what the original question was and reply "foo +is baz" instead of "bar is baz". + +Next we have some examples of special codes. If we have: + + foo is bar|baz|foo to you too|foos|$who + baz is foo + +...then the following are valid responses when asked about foo: + + foo is bar + baz is foo + foo to you too + * A foos + foo is z + +-- end --