# -*- 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, []], ['eagerToHelp', 1, 1, 1], # whether to even need the "?" on questions ['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, []], ['prefixes', 1, 1, ['', 'I have heard that ', '', 'Maybe ', 'I seem to recall that ', '', 'iirc, ', '', 'Was it not... er, someone, who said: ', '', 'Well, ', 'um... ', 'Oh, I know this one! ', '', 'everyone knows that! ', '', 'hmm... I think ', 'well, duh. ']], ['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 { # 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) unless $event->{'from'} eq $event->{'nick'}; } elsif ($event->{'channel'} eq '' and $message =~ /^:INFOBOT:QUERY <(\S+)> (.*)$/) { $self->ReceivedQuery($event, $2, $1) unless $event->{'from'} eq $event->{'nick'}; } elsif ($event->{'channel'} eq '' and $message =~ /^:INFOBOT:REPLY <(\S+)> (.+?) =(is|are)?=> (.*)$/) { $self->ReceivedReply($event, $3, $2, $1, $4) unless $event->{'from'} eq $event->{'nick'}; } elsif ($message =~ /^\s*literal\s+(.+?)\s*$/) { $self->Literal($event, $1); } elsif ($event->{level} < 10) { # make this module a very low priority return 10; } 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) = @_; return 10 unless $event->{level} >= 10; # make this module a very low priority 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) = @_; return 10 unless $event->{level} >= 10; # make this module a very low priority 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* (?:\w+[:.!\s]+\s+)? (?:(?:well|and|or|yes|[uh]+m*|o+[oh]*[k]+(?:a+y+)?|still|well|so|a+h+|o+h+)[:,.!?\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]+|)* (?:tell\s+me[,\s]+)? (?:(?:(?:stupid\s+)?q(?:uestion)?|basically)[:,.!\s]+)* (?:tell\s+me[,\s]+)? (?:(?:does\s+)?(?:any|ne)\s*(?:1|one|body)\s+know[,\s]+|)? (.*) \s*$/osix) { $shortMessage = $1; } $self->debug("message: '$message'"); $self->debug("shortMessage: '$shortMessage'"); 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+the\s+hell|\s+on\s+earth|\s+the\s+fuck)? \s+ (is|are) \s+ (.+?) [?!\s]* $/osix) { $self->GiveFactoid($event, lc($1), # is/are (optional) $2, # subject $direct); } elsif ($shortMessage =~ /^(?:(?:where|how) (?:\s+the\s+hell|\s+on\s+earth|\s+the\s+fuck)? \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 =~ /^(.+?) \s+ (is|are) \s+ (?:what|where|who) [?!\s]* $/osix) { $self->GiveFactoid($event, lc($2), # is/are (optional) $1, # subject $direct); } elsif ($shortMessage =~ /^(?:what|where|who) (?:\s+the\s+hell|\s+on\s+earth|\s+the\s+fuck)? \s+ (?:am\s+I|I\s+am) [?\s]* $/osix) { $self->GiveFactoid($event, 'is', # am => is $event->{'from'}, # subject $direct); } elsif ($shortMessage =~ /^(no\s*, (\s*\Q$event->{'nick'}\E\s*,)? \s+)? (?:remember\s*:\s+)? (.+?) \s+ (is|are) \s+ (also\s+)? (.*?[^?\s]) \s* $/six) { # the "remember:" prefix can be used to delimit the start of the actual content, if necessary. $self->SetFactoid($event, defined($1) && ($direct || defined($2)), # replace existing answer? $3, # subject lc($4), # is/are defined($5), # add to existing answer? $6, # object $direct || defined($2)); } elsif ($shortMessage =~ /^(no\s*, (?:\s*\Q$event->{'nick'}\E\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) if ($3 or ($direct == 2 and $self->{'eagerToHelp'})); } elsif ((not $direct or $direct == 2) and $shortMessage =~ /^(.+?)[?!.\s]*(\?)?[?!.\s]*$/osi) { $self->GiveFactoid($event, undef, # is/are (optional) $1, # subject $direct) if ($2 or ($direct == 2 and $self->{'eagerToHelp'})); } 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 =~ m/^$event->{'nick'}$/i) { $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 { # pick a 'random' prefix my $prefix = $self->{'prefixes'}->[$event->{'time'} % @{$self->{'prefixes'}}]; if (lc($who) eq lc($subject)) { $answer = "${prefix}you are $answer"; } else { $answer = "$prefix$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, $event->{'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'}}) { next if $bot eq $event->{'nick'}; 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, {}, $event->{'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 = $event->{'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)) { $self->say($event, "$event->{from}: $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); } }