File Coverage

blib/lib/Bot/BasicBot/Pluggable/Module/Infobot.pm
Criterion Covered Total %
statement 171 229 74.6
branch 85 134 63.4
condition 43 70 61.4
subroutine 20 24 83.3
pod 4 16 25.0
total 323 473 68.2


line stmt bran cond sub pod time code
1             package Bot::BasicBot::Pluggable::Module::Infobot;
2             $Bot::BasicBot::Pluggable::Module::Infobot::VERSION = '1.20';
3 2     2   8 use strict;
  2         2  
  2         49  
4 2     2   6 use warnings;
  2         27  
  2         50  
5 2     2   7 use base qw(Bot::BasicBot::Pluggable::Module);
  2         2  
  2         134  
6              
7 2     2   9 use Data::Dumper;
  2         2  
  2         110  
8 2     2   541 use LWP::UserAgent ();
  2         27582  
  2         32  
9 2     2   8 use URI;
  2         7  
  2         95  
10              
11             # this one is a complete bugger to build
12             eval { require XML::Feed };
13             our $HAS_XML_FEED = $@ ? 0 : 1;
14 2     2   7 use constant PROT => "ibprot_";
  2         2  
  2         102  
15 2     2   7 use constant INFO => "infobot_";
  2         2  
  2         5107  
16              
17             sub init {
18 2     2 1 6 my $self = shift;
19 2         32 $self->config(
20             {
21             user_allow_searching => 0,
22             user_min_length => 3,
23             user_max_length => 25,
24             user_num_results => 20,
25             user_passive_answer => 0,
26             user_passive_learn => 0,
27             user_require_question => 1,
28             user_http_timeout => 10,
29             user_rss_items => 5,
30             user_stopwords =>
31             "here|how|it|something|that|this|what|when|where|which|who|why",
32             user_unknown_responses =>
33             "Dunno.|I give up.|I have no idea.|No clue. Sorry.|Search me, bub.|Sorry, I don't know.",
34             db_version => "1",
35             }
36             );
37              
38             # record what we've asked other bots.
39 2         9 $self->{remote_infobot} = {};
40             }
41              
42             sub help {
43             return
44 1     1 1 4 "An infobot. See http://search.mcpan.org/perldoc?Bot::BasicBot::Pluggable::Module::Infobot.";
45             }
46              
47             sub told {
48 75     75 1 63 my ( $self, $mess ) = @_;
49 75         105 local $self->{mess} = $mess;
50 75         90 my $body = $mess->{body};
51 75 50       110 return unless defined $body;
52              
53             # looks like an infobot reply.
54 75 50       158 if ( $body =~ s/^:INFOBOT:REPLY (\S+) (.*)$// ) {
55 0         0 return $self->infobot_reply( $1, $2, $mess );
56             }
57              
58             # direct commands must be addressed.
59 75 100       150 return unless $mess->{address};
60              
61             # forget a particular factoid.
62 64 100       142 if ( $body =~ /^forget\s+(.*)$/i ) {
63 3 50       7 unless ($self->protection_status($mess, $1)) {
64 3 100       7 return $self->delete_factoid($1)
65             ? "I forgot about $1."
66             : "I don't know anything about $1.";
67             }
68             }
69              
70             # ask another bot for facts.
71 61 50       97 if ( $body =~ /^ask\s+(\S+)\s+about\s+(.*)$/i ) {
72 0         0 $self->ask_factoid( $2, $1, $mess );
73 0         0 return "I'll ask $1 about $2.";
74             }
75              
76             # tell someone else about a factoid
77 61 100       91 if ( $body =~ /^tell\s+(\S+)\s+about\s+(.*)$/i ) {
78 1         5 $self->tell_factoid( $2, $1, $mess );
79 1         297 return "Told $1 about $2.";
80             }
81              
82             # search for a particular factoid.
83 60 100       96 if ( $body =~ /^search\s+for\s+(.*)$/i ) {
84 6 100       22 return "privmsg only, please" unless ( $mess->{channel} eq "msg" );
85 5 100       11 return "searching disabled" unless $self->get("user_allow_searching");
86 4         16 my @results = $self->search_factoid( split( /\s+/, $1 ) );
87 4 100       9 unless (@results) { return "I don't know anything about $1."; }
  1         6  
88 3 50       8 $#results = $self->get("user_num_results") - 1
89             unless $#results < $self->get("user_num_results");
90 3         6 return "I know about: " . join( ", ", map { "'$_'" } @results ) . ".";
  4         16  
91             }
92              
93 54 50       114 if ($self->authed( $mess->{who} )) {
94             # protect a particular factoid.
95 0 0       0 if ( $body =~ /^protect\s+(.*)$/i ) {
96 0 0       0 return $self->protect_factoid($1)
97             ? "Protected $1."
98             : "Already protected.";
99             }
100              
101             # unprotect a particular factoid.
102 0 0       0 if ( $body =~ /^unprotect\s+(.*)$/i ) {
103 0 0       0 return $self->unprotect_factoid($1)
104             ? "Unprotected $1."
105             : "Was not protected";
106             }
107             }
108             }
109              
110             sub fallback {
111 65     65 1 62 my ( $self, $mess ) = @_;
112 65         79 local $self->{mess} = $mess;
113 65   50     139 my $body = $mess->{body} || "";
114              
115 65   33     210 my $is_priv = !defined $mess->{channel} || $mess->{channel} eq 'msg';
116              
117             # request starts with "my", so we'll look for
118             # a valid factoid for "$mess->{who}'s $object".
119 65         58 $body =~ s/^my /$mess->{who}'s /;
120              
121             my %stopwords =
122 65         149 map { lc($_) => 1 }
  758         1041  
123             split( /\s*[\s,\|]\s*/, $self->get("user_stopwords") );
124              
125             # checks to see if something starts
126             # <word> (is|are)
127             # and then removes if if <word> is a stopword
128             # this means that we treat "what is foo?" as "foo?"
129 65 100       361 if ( $body =~ /^(.*?)\s+(is|are)\s+(.*)$/i ) {
130 30 100       94 $body =~ s/^(.*?)\s+(is|are)\s+//i if $stopwords{$1};
131             }
132              
133             # answer a factoid. this is a crazy check which ensures we will ONLY answer
134             # a factoid if a) there is, or isn't, a question mark, b) we have, or haven't,
135             # been addressed, c) the factoid is bigger and smaller than our requirements,
136             # and d) that it doesn't look like a to-be-learned factoid (which is important
137             # if the user has disabled the requiring of the question mark for answering.)
138 65 50 33     261 my $readdress = $mess->{channel} ne 'msg' && $body =~ s/\s+@\s+(\S+)[.]?\s*$// ? $1 : '';
139 65 50 33     117 my $body_regexp =
140             $self->get("user_require_question") && !$is_priv ? qr/\?+$/ : qr/[.!?]*$/;
141 65 50 100     404 if ( $body =~ s/$body_regexp//
      66        
      66        
      66        
      33        
142             and ( $mess->{address} or $self->get("user_passive_answer") )
143             and length($body) >= $self->get("user_min_length")
144             and length($body) <= $self->get("user_max_length")
145             and $body !~ /^(.*?)\s+(is|are)\s+(.*)$/i )
146             {
147              
148             # get the factoid and type of relationship
149 36         61 my ( $is_are, $factoid, $literal ) = $self->get_factoid($body);
150 36 50 100     225 if ( !$literal && $factoid && $factoid =~ /\|/ ) {
      66        
151 0         0 my @f = split /\|/, $factoid;
152 0         0 $factoid = $f[ int( rand( scalar @f ) ) ];
153             }
154              
155             # no factoid?
156 36 100       57 unless ($factoid) {
157 4         9 my @unknowns = split( /\|/, $self->get("user_unknown_responses") );
158 4         11 my $unknown = $unknowns[ int( rand( scalar(@unknowns) ) ) - 1 ];
159 4 50       28 return $mess->{address} ? $unknown : undef;
160             }
161              
162             # variable substitution.
163 32         36 $factoid =~ s/\$who/$mess->{who}/g;
164              
165             # emote?
166 32 50       66 if ( $factoid =~ s/^<action>\s*//i ) {
    100          
167             $self->bot->emote(
168             {
169             who => $readdress || $mess->{who},
170             channel => $mess->{channel},
171 0   0     0 body => $factoid
172             }
173             );
174 0         0 return 1;
175              
176             # replying with, or without a noun? hmMmMmmm?!
177             }
178             elsif ($literal) {
179 4         9 $body =~ s!^literal\s+!!;
180 4         10 $factoid = "$body =${is_are}= $factoid";
181             }
182             else {
183 28 100       75 $factoid = $factoid =~ s/^<reply>\s*//i
184             ? $factoid
185             : "$body $is_are $factoid";
186             }
187 32 50       42 if ($readdress) {
188 0         0 my %hash = %$mess;
189 0         0 $hash{who} = $readdress;
190 0         0 $self->reply(\%hash, $factoid);
191 0         0 return 1;
192             }
193 32         213 return $factoid;
194             }
195              
196             # the only thing left is learning factoids. are we
197             # addressed or are we willing to learn passively?
198             # does it even look like a factoid?
199 29 100 100     64 return unless ( $mess->{address} or $self->get("user_passive_learn") );
200             return
201 26 100 100     141 unless ( $body =~ /^(.*?)\s+(is)\s+(.*)$/i
202             or $body =~ /^(.*?)\s+(are)\s+(.*)$/i );
203 24         108 my ( $object, $is_are, $description ) = ( $1, $2, $3 );
204 24         29 my $literal = ( $object =~ s!^literal\s+!! );
205              
206             # allow corrections and additions.
207 24         61 my ( $nick, $replace, $also ) = ( $self->bot->nick, 0, 0 );
208 24 100       213 $replace = 1 if ( $object =~ s/no,?\s+//i ); # no, $object is $fact.
209 24 50 66     108 $replace = 1
210             if ( $replace and $object =~ s/^\s*$nick,?\s*//i )
211             ; # no, $bot, $object is $fact.
212 24 100       56 $also = 1 if ( $description =~ s/^also\s+//i ); # $object is also $fact.
213              
214             # ignore short, long, and stopword'd factoids.
215 24 50       50 return if length($object) < $self->get("user_min_length");
216 24 100       41 return if length($object) > $self->get("user_max_length");
217 23 50       68 foreach ( keys %stopwords ) { return if $object =~ /^$_\b/; }
  276         1675  
218              
219             # if we're replacing things, remove the factoid first.
220             # $also check supports "no, $bot, $object is also $fact".
221 23 100 66     63 if ( $replace and !$also ) {
222 1 50       4 unless ($self->protection_status($mess, $object)) {
223 1         3 $self->delete_factoid($object);
224             }
225             }
226              
227             # get any current factoid there might be.
228 23         37 my ( $type, $current ) = $self->get_factoid($object);
229              
230             # we can't add without explicit instruction,
231             # but shouldn't warn if this is passive.
232 23 100 100     149 if ( $current and !$also and $mess->{address} ) {
    100 66        
      100        
      66        
233 2         22 return "... but $object $type $current ...";
234             }
235             elsif ( $current and !$also and !$mess->{address} ) {
236 2         14 return;
237             }
238              
239 19 50       39 unless ( $self->protection_status($mess, $object) ) {
240             # add this factoid. this comment is absolutely useless. excelsior.
241 19         61 $self->add_factoid( $object, $is_are, split( /\s+or\s+/, $description ) );
242              
243             # return an ack if we were addressed only
244 19 50       134 return $mess->{address} ? "Okay." : 1;
245             }
246             }
247              
248             sub get_factoid {
249 60     60 0 64 my ( $self, $object ) = @_;
250              
251 60         69 my $literal = ( $object =~ s!^literal\s+!! );
252              
253             # get a list of factoid hashes
254 60         95 my ( $is_are, @factoids ) = $self->get_raw_factoids($object);
255              
256 60 100       107 return unless @factoids;
257              
258             #print STDERR Dumper(@factoids);
259              
260             # simple is a list of the 'simple' factoids, a is b, etc. These are just
261             # joined together. Alternates are factoids that are an alternative to
262             # the simple factoids, they will randomly be displayed _instead_.
263 46         38 my ( @simple, @alternatives );
264              
265 46         59 for (@factoids) {
266 125 50       238 next if $_->{text} =~ m!^\s*$!;
267 125 100 66     241 if ( $_->{alternate} || $_->{alt} ) {
268 25         27 push @alternatives, $_->{text};
269             }
270             else {
271 100         119 push @simple, $_->{text};
272             }
273             }
274              
275 46 100       62 if ($literal) {
276 4         8 my $return .= join " =or= ", ( @simple, map { "|$_" } @alternatives );
  3         9  
277 4         14 return ( $is_are, $return, 1 );
278             }
279              
280             #print STDERR Dumper(@alternatives);
281              
282             # the simple list is one of the alternatives
283 42 50       105 unshift( @alternatives, join( " or ", @simple ) ) if @simple;
284              
285             # pick an option at random
286 42         876 srand();
287 42         104 my $factoid = $alternatives[ rand(@alternatives) ];
288              
289             #print STDERR "$factoid\n";
290             # if there are any RSS directives, get the feed.
291             # TODO - this could be done in a more general way, with plugins
292             # TODO - this blocks. Bad. you can knock the bot off channel by
293             # giving it an RSS feed that'll take a very long time to return.
294 42         64 $factoid =~
295 2         8 s/<(?:rss|atom|feed|xml)\s*=\s*\"?([^>\"]+)\"?>/$self->parseFeed($1)/ieg;
296              
297 42         135 return ( $is_are, $factoid, 0 );
298             }
299              
300             # for a given key, return the raw hashes that are in the store for this
301             # factoid.
302             sub get_raw_factoids {
303 79     79 0 68 my ( $self, $object ) = @_;
304 79 100       191 my $raw = $self->get( INFO . lc($object) )
305             or return ();
306              
307             #print STDERR Dumper($raw);
308 55         65 my ( $is_are, @factoids );
309              
310 55 50       116 if ( ref($raw) ) {
311              
312             # it's a deep structure
313 55         52 $is_are = $raw->{is_are};
314             @factoids = map {
315 144 50 33     560 ref $_ && /DBM::Deep::Hash/ ? +{ %$_ } : $_
316 55 50       38 } @{ $raw->{factoids} || [] };
  55         121  
317              
318             }
319             else {
320              
321             # old-style tab seperated thing
322 0         0 my @strings;
323 0         0 ( $is_are, @strings ) = split( /\t/, $raw );
324 0         0 for my $text (@strings) {
325 0 0       0 my $alt = ( $text =~ s/^\|\s*// ? 1 : 0 );
326 0         0 push @factoids, { alternate => $alt, text => $text };
327             }
328             }
329              
330 55         121 return ( $is_are, @factoids );
331             }
332              
333             sub add_factoid {
334 19     19 0 29 my ( $self, $object, $is_are, @factoids ) = @_;
335              
336             # get the current list, if any
337 19         30 my ( $current_is_are, @current ) = $self->get_raw_factoids($object);
338              
339             # if there's already an is_are set, use it.
340 19 100       35 $is_are = $current_is_are if ($current_is_are);
341 19   50     34 $is_are ||= "is"; # defaults
342              
343             # add these factoids to the list, trimming trailing space after |
344 19         28 for (@factoids) {
345 20 100       47 my $alt = s/^\|\s*// ? 1 : 0;
346 20         62 push @current,
347             {
348             alternate => $alt,
349             text => $_,
350             };
351             }
352              
353 19         47 my $set = {
354             is_are => $is_are,
355             factoids => \@current,
356             };
357              
358             # put the list back into the store.
359 19         55 $self->set( INFO . lc($object), $set );
360              
361 19         29 return 1;
362             }
363              
364             sub protection_status {
365 23     23 0 23 my $self = shift;
366 23         25 my ($mess, $object) = @_;
367             $self->get( PROT . lc($object) ) && !$self->authed( $mess->{who} )
368 23 50       66 }
369              
370             sub delete_factoid {
371 4     4 0 7 my ( $self, $object ) = @_;
372 4         6 my $key = INFO . lc($object);
373 4 100       9 if ( $self->get($key) ) {
374 3         15 $self->unset( INFO . lc($object) );
375 3         16 return 1;
376             }
377             else {
378 1         7 return 0;
379             }
380             }
381              
382             sub protect_factoid {
383 0     0 0 0 my ( $self, $object ) = @_;
384 0         0 my $key = PROT . lc($object);
385 0 0       0 unless ( $self->get($key) ) {
386 0         0 $self->set( $key, 1 );
387 0         0 return 1;
388             }
389             else {
390 0         0 return 0;
391             }
392             }
393              
394             sub unprotect_factoid {
395 0     0 0 0 my ( $self, $object ) = @_;
396 0         0 my $key = PROT . lc($object);
397 0 0       0 if ( $self->get($key) ) {
398 0         0 $self->unset( $key );
399 0         0 return 1;
400             }
401             else {
402 0         0 return 0;
403             }
404             }
405              
406             sub ask_factoid {
407 0     0 0 0 my ( $self, $object, $ask, $mess ) = @_;
408              
409             # unique ID to reference this in future
410 0         0 my $id = "<" . int( rand(100000) ) . ">";
411              
412             # store the message, so we can reply in context later
413 0         0 $self->{remote_infobot}{$id} = $mess;
414              
415             # ask, using an infobot protocol, the thing we've been told to ask.
416             # this will hopefully result in a reply coming back later.
417 0         0 $self->bot->say(
418             who => $ask,
419             channel => 'msg',
420             body => ":INFOBOT:QUERY $id $object"
421             );
422             }
423              
424             sub tell_factoid {
425 1     1 0 3 my ( $self, $object, $tell, $mess ) = @_;
426              
427 1         4 my ( $is_are, $factoid ) = $self->get_factoid($object);
428 1         3 my $from = $mess->{who};
429              
430 1         4 $self->bot->say(
431             who => $tell,
432             channel => 'msg',
433             body => "$from wanted you to know: $object $is_are $factoid"
434             );
435             }
436              
437             sub search_factoid {
438 4     4 0 8 my ( $self, @terms ) = @_;
439 4         4 my @keys;
440 4         4 for (@terms) {
441             push @keys,
442 6 50       14 map { my $term = $_; $term =~ s/^${\(INFO)}// ? $term : () }
  4         2  
  4         5  
  4         29  
443             $self->store_keys(
444             limit => $self->get("user_num_results"),
445             res => ["$_"]
446             );
447             }
448 4         6 return @keys;
449             }
450              
451             sub parseFeed {
452 2     2 0 8 my ( $self, $url ) = @_;
453              
454 2         3 my @items;
455 2         3 eval {
456 2         16 my $ua = LWP::UserAgent->new();
457 2         2605 $ua->timeout( $self->get('user_http_timeout') );
458 2         29 $ua->env_proxy;
459 2         2918 my $feed;
460 2         8 my $response = $ua->get($url);
461 2 50       22010 if ( $response->is_success ) {
462 2 50       20 $feed = XML::Feed->parse( \$response->content() )
463             or die XML::Feed->errstr . "\n";
464             }
465             else {
466 0         0 die $response->status_line() . "\n";
467             }
468 0         0 my @entries = $feed->entries();
469 0         0 my $max_items = $self->get('user_rss_items');
470 0 0 0     0 if ( $max_items and $max_items < @entries ) {
471 0         0 splice( @entries, $max_items );
472             }
473 0         0 @items = map { $_->title } @entries;
  0         0  
474             };
475 2 50       196 if ($@) {
476 2         5 chomp $@;
477 2         11 return "<< Error parsing RSS from $url: $@ >>";
478             }
479              
480 0           my $ret;
481 0           foreach my $title (@items) {
482 0           $title =~ s/\s+/ /;
483 0           $title =~ s/\n//g;
484 0           $title =~ s/\s+$//;
485 0           $title =~ s/^\s+//;
486 0           $ret .= "${title}; ";
487             }
488 0           $ret =~ s/\s*;\s*$//;
489 0 0         return ( $ret =~ m/^<(reply|action)>/ ? $ret : "<reply>$ret" );
490             }
491              
492             # We've been replied to by an infobot.
493             sub infobot_reply {
494 0     0 0   my ( $self, $id, $return, $mess ) = @_;
495              
496             # get the message that caused the ask initially, so we can reply to it
497             # if there wasn't one, just give up.
498 0 0         my $infobot_data = $self->{remote_infobot}{$id} or return 1;
499              
500             # this is the string that the other infobot returned to us.
501 0           my ( $object, $db, $factoid ) = ( $return =~ /^(.*) =(\w+)=> (.*)$/ );
502              
503 0           $self->set_factoid( $mess->{who}, $object, $db, $factoid );
504              
505             # reply to the original request saying 'we got it'
506             $self->bot->say(
507             channel => $infobot_data->{channel},
508             who => $infobot_data->{who},
509 0           body => "Learnt about $object from $mess->{who}",
510             );
511              
512 0           return 1;
513              
514             }
515              
516             1;
517              
518             __END__
519              
520             =head1 NAME
521              
522             Bot::BasicBot::Pluggable::Module::Infobot - infobot clone redone in B::B::P.
523              
524             =head1 VERSION
525              
526             version 1.20
527              
528             =head1 SYNOPSIS
529              
530             Does infobot things - basically remembers and returns factoids. Will ask
531             another infobot about factoids that it doesn't know about, if you want. Due
532             to persistent heckling from the peanut gallery, does things pretty much
533             exactly like the classic infobot, even when they're not necessarily that
534             useful (for example, saying "Okay." rather than "OK, water is wet."). Further
535             infobot backwards compatibility is available through additional packages
536             such as L<Bot::BasicBot::Pluggable::Module::Foldoc>.
537              
538             =head1 IRC USAGE
539              
540             The following examples assume you're running Infobot with its defaults settings,
541             which require the bot to be addressed before learning factoids or answering
542             queries. Modify these settings with the Vars below.
543              
544             <user> bot: water is wet.
545             <bot> user: okay.
546             <user> bot: water?
547             <bot> user: water is wet.
548             <user> bot: water is also blue.
549             <bot> user: okay.
550             <user> bot: water?
551             <bot> user: water is wet or blue.
552             <user> bot: no, water is translucent.
553             <bot> user: okay.
554             <user> bot: water?
555             <bot> user: water is translucent.
556             <user> bot: forget water.
557             <bot> user: I forgot about water.
558             <user> bot: water?
559             <bot> user: No clue. Sorry.
560              
561             A fact that begins with "<reply>" will have the "<noun> is" stripped:
562              
563             <user> bot: what happen is <reply>somebody set us up the bomb.
564             <bot> user: okay.
565             <user> bot: what happen?
566             <bot> user: somebody set us up the bomb.
567              
568             A fact that begins "<action>" will be emoted as a response:
569              
570             <user> bot: be funny is <action>dances silly.
571             <bot> user: okay.
572             <user> bot: be funny?
573             * bot dances silly.
574              
575             Pipes ("|") indicate different possible answers, picked at random:
576              
577             <user> bot: dice is one|two|three|four|five|six
578             <bot> user: okay.
579             <user> bot: dice?
580             <bot> user: two.
581             <user> bot: dice?
582             <bot> user: four.
583            
584             You can also use RSS feeds as a response:
585              
586             <user> bot: jerakeen.org is <rss="http://jerakeen.org/rss">.
587             <bot> user: okay.
588             <user> bot: jerakeen.org?
589             <bot> user: jerakeen.org is <item>; <item>; etc....
590              
591             You can also ask the bot to learn a factoid from another bot, as follows:
592              
593             <user> bot: ask bot2 about fact.
594             <bot> user: asking bot2 about fact...
595             <user> bot: fact?
596             <bot> user: fact is very boring.
597              
598             Finally, you can privmsg the bot to search for particular facts:
599              
600             <user> search for options.
601             <bot> I know about 'options indexes', 'charsetoptions override', etc....
602              
603             =head1 METHODS
604              
605              
606              
607             =head1 VARS
608              
609             =over 4
610              
611             =item min_length
612              
613             Defaults to 3; the minimum length a factoid, or inquiry, must be before recognizing it.
614              
615             =item max_length
616              
617             Defaults to 25; the maximum length a factoid, or inquiry, can be before ignoring it.
618              
619             =item num_results
620              
621             Defaults to 20; the number of facts to return for "search for <term>" privmsg.
622              
623             =item passive_answer
624              
625             Defaults to 0; when enabled, the bot will answer factoids without being addressed.
626              
627             =item passive_learn
628              
629             Defaults to 0; when enabled, the bot will learn factoids without being addressed.
630              
631             =item require_question
632              
633             Defaults to 1; determines whether the bot requires a question mark before
634             responding to a factoid. When enabled, the question mark is required (ie. "water?").
635             When disabled, the question mark is entirely optional (ie. "water" would also
636             produce a response).
637              
638             =item stopwords
639              
640             A comma-, space-, or pipe- separated list of words the bot should not learn or
641             answer. This prevents such insanity as the learning of "where is the store?" and
642             "how is your mother?" The default list of stopwords contains "here", "how", "it",
643             "something", "that", "this", "what", "when", "where", "which", "who" and "why").
644              
645             =item unknown_responses
646              
647             A pipe-separated list of responses the bot will randomly choose from when it
648             doesn't know the answer to a question. The default list of response contains
649             "Dunno.", "I give up.", "I have no idea.", "No clue. Sorry.", "Search me, bub.",
650             and "Sorry, I don't know."
651              
652             =item allow_searching
653              
654             Defaults to 0.
655              
656             Searching on large factoid lists is ... problematic.
657              
658             =item http_timeout
659              
660             Time in seconds for an http request to timeout. When this value is
661             set to a very high value, a slow site can disconnect a bot by
662             blocking it. Defaults to 10.
663              
664             =item rss_items
665              
666             Maximal numbers of items returns when using RSS feeds. Defaults to 5.
667              
668             =back
669              
670             =head1 BUGS
671              
672             "is also" doesn't work on <reply>s (ie. "<bot> cheetahs! or <reply>monkies.")
673              
674             "is also" doesn't work on <action>s (same as the previous bug, hobo.)
675              
676             The pipe syntax for random replies doesn't actually work. At all. Um.
677              
678             We should probably make a "choose_random_response" function.
679              
680             "<bot>?" fails, due to removal of <bot> name from $mess->body.
681              
682             "ask" syntax doesn't work in a private message.
683              
684             The tab stops are set to 2, not 4. OHMYGOD.
685              
686             If a "search" fails, the bot doesn't tell you.
687              
688             "search" is case-sensitive.
689              
690             If Title module is loaded, <rss> factoids don't work cos of told/fallback.
691              
692             =head1 REQUIREMENTS
693              
694             URI
695              
696             L<LWP::Simple>
697              
698             L<XML::Feed>
699              
700             =head1 AUTHOR
701              
702             Mario Domgoergen <mdom@cpan.org>
703              
704             This program is free software; you can redistribute it
705             and/or modify it under the same terms as Perl itself.