File Coverage

lib/Bot/BasicBot/Pluggable.pm
Criterion Covered Total %
statement 161 219 73.5
branch 34 72 47.2
condition 10 18 55.5
subroutine 36 48 75.0
pod 18 21 85.7
total 259 378 68.5


line stmt bran cond sub pod time code
1             package Bot::BasicBot::Pluggable;
2             $Bot::BasicBot::Pluggable::VERSION = '1.10';
3 10     10   70997 use warnings;
  10         17  
  10         284  
4 10     10   36 use strict;
  10         13  
  10         162  
5              
6 10     10   114 use 5.8.0;
  10         25  
7 10     10   4591 use POE;
  10         293919  
  10         54  
8 10     10   461474 use Bot::BasicBot 0.60;
  10         1323995  
  10         832  
9 10     10   7105 use Log::Log4perl;
  10         315723  
  10         57  
10 10     10   506 use Log::Log4perl::Level;
  10         16  
  10         95  
11 10     10   812 use base qw( Bot::BasicBot );
  10         13  
  10         856  
12 10     10   5760 use Data::Dumper;
  10         55164  
  10         700  
13              
14             $Data::Dumper::Terse = 1;
15             $Data::Dumper::Indent = 0;
16              
17             use Module::Pluggable
18 10         61 sub_name => '_available',
19             search_path => 'Bot::BasicBot::Pluggable::Module',
20 10     10   3907 except => 'Bot::BasicBot::Pluggable::Module::Base';
  10         67551  
21 10     10   4680 use Bot::BasicBot::Pluggable::Module;
  10         23  
  10         273  
22 10     10   3256 use Bot::BasicBot::Pluggable::Store;
  10         22  
  10         279  
23 10     10   47 use File::Spec;
  10         10  
  10         195  
24 10     10   31 use Try::Tiny;
  10         10  
  10         4206  
25              
26             sub init {
27 8     8 1 121 my $self = shift;
28 8         37 $self->init_logging();
29              
30 8         23100 my $logger = Log::Log4perl->get_logger( ref $self );
31 8         180 $logger->info( 'Starting initialization of ' . ref $self );
32              
33 8 50       61 if ( !$self->store ) {
    100          
34 0         0 $logger->debug('Store not set, trying to load a store backend');
35 0         0 my $store;
36 0         0 for my $type (qw( DBI Deep Storable Memory )) {
37             $store = try {
38 0     0   0 $logger->debug("Trying to load store backend $type");
39 0         0 Bot::BasicBot::Pluggable::Store->new( { type => $type } );
40 0         0 };
41 0 0       0 if ($store) {
42 0         0 $logger->info("Loaded store backend $type");
43 0         0 last;
44             }
45             }
46 0 0       0 if ( !UNIVERSAL::isa( $store, 'Bot::BasicBot::Pluggable::Store' ) ) {
47 0         0 $logger->logdie("Couldn't load any default store type");
48             }
49 0         0 $self->store($store);
50             }
51             elsif ( !UNIVERSAL::isa( $self->store, "Bot::BasicBot::Pluggable::Store" ) )
52             {
53 5         15 $self->store( Bot::BasicBot::Pluggable::Store->new( $self->store ) );
54             }
55 8         26 return 1;
56             }
57              
58             sub init_logging {
59 8     8 0 13 my $self = shift;
60 8         61 my $logger = Log::Log4perl->get_logger( ref $self );
61 8 50       2052 if ( $self->logconfig ) {
62 0         0 Log::Log4perl->init( $self->logconfig );
63             }
64             else {
65 8         32 my $loglevel = $self->loglevel;
66 8         65 Log::Log4perl::init( \ <<EOT );
67             log4perl.rootLogger=$loglevel,Screen
68             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
69             log4perl.appender.Screen.stderr = 0
70             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
71             log4perl.appender.Screen.layout.ConversionPattern = %-6p %d %m%n
72             EOT
73             }
74             }
75              
76             sub log {
77 0     0 1 0 my $self = shift;
78 0         0 my $logger = Log::Log4perl->get_logger( ref $self );
79 0         0 for my $log_entry (@_) {
80 0         0 chomp $log_entry;
81 0         0 $logger->warn($log_entry);
82             }
83 0         0 return;
84             }
85              
86             sub load {
87 14     14 1 625 my $self = shift;
88 14         18 my $module = shift;
89              
90 14         49 my $logger = Log::Log4perl->get_logger( ref $self );
91              
92             # it's safe to die here, mostly this call is eval'd.
93 14 50       237 $logger->logdie("Cannot load module without a name") unless $module;
94 14 50       51 $logger->logdie("Module $module already loaded") if $self->handler($module);
95              
96             # This is possible a leeeetle bit evil.
97 14         46 $logger->info("Loading module $module");
98 14         67 my $filename = $module;
99 14         23 $filename =~ s{::}{/}g;
100 14         26 my $file = "Bot/BasicBot/Pluggable/Module/$filename.pm";
101 14 100       215 $file = "./$filename.pm" if ( -e "./$filename.pm" );
102 14 100       105 $file = "./modules/$filename.pm" if ( -e "./modules/$filename.pm" );
103 14         90 $logger->debug("Loading module $module from file $file");
104 14         1086 warn "Loading $module from $file";
105              
106             # force a reload of the file (in the event that we've already loaded it).
107 10     10   47 no warnings 'redefine';
  10         14  
  10         13278  
108 14         98 delete $INC{$file};
109              
110 14     9   162 try { require $file } catch { die "Can't load $module: $_"; };
  14         5833  
  0         0  
111              
112             # Ok, it's very evil. Don't bother me, I'm working.
113              
114 14         500 my $m = "Bot::BasicBot::Pluggable::Module::$module"->new(
115             Bot => $self,
116             Param => \@_
117             );
118              
119 14 50 33     87 $logger->logdie("->new didn't return an object") unless ( $m and ref($m) );
120 14 50       183 $logger->logdie( ref($m) . " isn't a $module" )
121             unless ref($m) =~ /\Q$module/;
122              
123 14         67 $self->add_handler( $m, $module );
124              
125 14         57 return $m;
126             }
127              
128             sub reload {
129 0     0 1 0 my $self = shift;
130 0         0 my $module = shift;
131 0         0 my $logger = Log::Log4perl->get_logger( ref $self );
132 0 0       0 $logger->logdie("Cannot reload module with a name") unless $module;
133 0 0       0 $self->remove_handler($module) if $self->handler($module);
134 0         0 return $self->load($module);
135             }
136              
137             sub unload {
138 0     0 1 0 my $self = shift;
139 0         0 my $module = shift;
140 0         0 my $logger = Log::Log4perl->get_logger( ref $self );
141 0 0       0 $logger->logdie("Need name") unless $module;
142 0 0       0 $logger->logdie("Not loaded") unless $self->handler($module);
143 0         0 $logger->info("Unloading module $module");
144 0         0 $self->remove_handler($module);
145             }
146              
147             sub module {
148 54     54 1 48 my $self = shift;
149 54         76 return $self->handler(@_);
150             }
151              
152             sub modules {
153 0     0 1 0 my $self = shift;
154 0         0 return $self->handlers(@_);
155             }
156              
157             sub available_modules {
158 1     1 1 9 my $self = shift;
159             my @local_modules =
160 1         107 map { substr( ( File::Spec->splitpath($_) )[2], 0, -3 ) } glob('./*.pm'),
  2         34  
161             glob('./modules/*.pm');
162             my @central_modules =
163             map {
164 1         10 my $mod = $_;
  11         3820  
165 11         16 $mod =~ s/^Bot::BasicBot::Pluggable::Module:://;
166 11         12 $mod;
167             } $self->_available();
168 1         6 my @modules = sort @local_modules, @central_modules;
169 1         6 return @modules;
170             }
171              
172             # deprecated methods
173             sub handler {
174 1059     1059 1 892 my ( $self, $name ) = @_;
175 1059         3174 return $self->{handlers}{ lc($name) };
176             }
177              
178             sub handlers {
179 961     961 0 736 my $self = shift;
180             my @keys = sort {
181 10         17 my $xa = $self->handler($a);
182 10         14 my $xb = $self->handler($b);
183             (
184 10 0 50     25 ($xb->get('user_priority') || $xb->get('priority') || 0)
      50        
185             <=>
186             ($xa->get('user_priority') || $xa->get('priority') || 0)
187             ) || ($a cmp $b)
188 961         649 } keys( %{ $self->{handlers} } );
  961         2055  
189 961 50       2041 return @keys if wantarray;
190 0         0 return \@keys;
191             }
192              
193             sub add_handler {
194 14     14 1 22 my ( $self, $handler, $name ) = @_;
195 14         87 my $logger = Log::Log4perl->get_logger( ref $self );
196 14 50       334 $logger->logdie("Need a name for adding a handler") unless $name;
197             $logger->logdie("Can't load a handler with a duplicate name $name")
198 14 50       48 if $self->{handlers}{ lc($name) };
199 14         44 $self->{handlers}{ lc($name) } = $handler;
200             }
201              
202             sub remove_handler {
203 0     0 1 0 my ( $self, $name ) = @_;
204 0         0 my $logger = Log::Log4perl->get_logger( ref $self );
205 0 0       0 $logger->logdie("Need a name for removing a handler") unless $name;
206             $logger->logdie("Hander $name not defined")
207 0 0       0 unless $self->{handlers}{ lc($name) };
208 0         0 $self->{handlers}{ lc($name) }->stop();
209 0         0 delete $self->{handlers}{ lc($name) };
210             }
211              
212             sub store {
213 1335     1335 1 1431 my $self = shift;
214 1335 100       1727 $self->{store_object} = shift if @_;
215 1335         2792 return $self->{store_object};
216             }
217              
218             sub loglevel {
219 11     11 1 211 my $self = shift;
220 11 100       40 $self->{loglevel} = shift if @_;
221 11   100     81 return ($self->{loglevel} and uc $self->{loglevel}) || 'WARN';
222             }
223              
224             sub logconfig {
225 11     11 1 168 my $self = shift;
226 11 100       36 $self->{logconfig} = shift if @_;
227 11         26 return $self->{logconfig};
228             }
229              
230             sub dispatch {
231 214     214 1 259 my ( $self, $method, @args ) = @_;
232 214         547 my $logger = Log::Log4perl->get_logger( ref $self );
233              
234 214         3873 $logger->info("Dispatching $method");
235 214         884 for my $who ( $self->handlers ) {
236             ## Otherwise we would see tick every five seconds
237 216 50       286 if ( $method eq 'tick' ) {
238 0         0 $logger->trace("Trying to dispatch $method to $who");
239             }
240             else {
241 216         430 $logger->debug("Trying to dispatch $method to $who");
242             }
243 216 50 33     847 $logger->trace( "... with " . Dumper(@args) )
244             if $logger->is_trace && @args;
245              
246 216 50       813 next unless $self->handler($who)->can($method);
247             try {
248 0 0   0   0 $logger->trace(
249             "Dispatching $method to $who with " . Dumper(@args) )
250             if $logger->is_trace;
251 0         0 $self->handler($who)->$method(@args);
252             }
253             catch {
254 0     0   0 $logger->warn($_);
255             }
256 0         0 }
257 214         263 return;
258             }
259              
260             sub help {
261 1     1 1 2 my $self = shift;
262 1         2 my $mess = shift;
263 1         6 $mess->{body} =~ s/^help\s*//i;
264 1         7 my $logger = Log::Log4perl->get_logger( ref $self );
265              
266 1 50       31 unless ( $mess->{body} ) {
    50          
267             return
268 0         0 "Ask me for help about: "
269             . join( ", ", $self->handlers() )
270             . " (say 'help <modulename>').";
271             }
272 0         0 elsif ( $mess->{body} eq 'modules' ) {
273 0         0 return "These modules are available for loading: "
274             . join( ", ", $self->available_modules );
275             }
276             else {
277 1 50       6 if ( my $handler = $self->handler( $mess->{body} ) ) {
278             try {
279 1     1   42 return $handler->help($mess);
280             }
281             catch {
282 0     0   0 $logger->warn(
283             "Error calling help for handler $mess->{body}: $_");
284             }
285 1         10 }
286             else {
287 0         0 return "I don't know anything about '$mess->{body}'.";
288             }
289             }
290             }
291              
292             #########################################################
293             # the following routines are lifted from Bot::BasicBot: #
294             #########################################################
295             sub tick {
296 0     0 1 0 my $self = shift;
297 0         0 $self->dispatch('tick');
298 0         0 return 5;
299             }
300              
301             sub dispatch_priorities {
302 230     230 0 226 my ( $self, $event, $mess ) = @_;
303 230         151 my $response;
304             my $who;
305              
306 230         569 my $logger = Log::Log4perl->get_logger( ref $self );
307 230         3771 $logger->info('Dispatching said event');
308              
309 230         1013 for my $priority ( 0 .. 3 ) {
310 747         910 for my $handler ( $self->handlers ) {
311 754         545 my $response;
312 754         1787 $logger->debug(
313             "Trying to dispatch said to $handler on priority $priority");
314 754 50 33     3216 $logger->trace( '... with arguments ' . Dumper($mess) )
315             if $logger->is_trace and $mess;
316             try {
317 754     754   14887 $response =
318             $self->handler($handler)->$event( $mess, $priority );
319             }
320             catch {
321 0     0   0 $logger->warn($_);
322 754         4681 };
323 754 100 100     7437 if ( $priority and $response ) {
324 213         585 $logger->debug("Response by $handler on $priority");
325 213 50       1110 $logger->trace( 'Response is ' . Dumper($response) )
326             if $logger->is_trace;
327 213 100       922 return if $response eq '1';
328 212         307 $self->reply( $mess, $response );
329 212         407 return;
330             }
331             }
332             }
333 17         43 return;
334             }
335              
336             sub reply {
337 214     214 1 272 my ( $self, $mess, @other ) = @_;
338 214         888 $self->dispatch( 'replied', {%$mess}, @other );
339 214 50       464 if ( $mess->{reply_hook} ) {
340 214         509 return $mess->{reply_hook}->( $mess, @other );
341             }
342             else {
343 0           return $self->SUPER::reply( $mess, @other );
344             }
345             }
346              
347             BEGIN {
348 10     10   31 my @dispatchable_events = (
349             qw/
350             connected chanjoin chanpart userquit nick_change
351             topic kicked
352             /
353             );
354 10         19 my @priority_events = (qw/ said emoted /);
355             {
356             ## no critic qw(ProhibitNoStrict)
357 10     10   179 no strict 'refs';
  10         15  
  10         839  
  10         13  
358 10         17 for my $event (@dispatchable_events) {
359             *$event = sub {
360 0     0   0 shift->dispatch( $event, @_ );
361 70         316 };
362             }
363 10         18 for my $event (@priority_events) {
364             *$event = sub {
365 230     230   400 shift->dispatch_priorities( $event, @_ );
366 20         297 };
367             }
368             }
369             }
370              
371             1; # sigh.
372              
373             __END__
374              
375             =head1 NAME
376              
377             Bot::BasicBot::Pluggable - extended simple IRC bot for pluggable modules
378              
379             =head1 VERSION
380              
381             version 1.10
382              
383             =head1 SYNOPSIS
384              
385             =head2 Creating the bot module
386              
387             # with all defaults.
388             my $bot = Bot::BasicBot->new();
389              
390             # with useful options. pass any option
391             # that's valid for Bot::BasicBot.
392             my $bot = Bot::BasicBot::Pluggable->new(
393            
394             channels => ["#bottest"],
395             server => "irc.example.com",
396             port => "6667",
397              
398             nick => "pluggabot",
399             altnicks => ["pbot", "pluggable"],
400             username => "bot",
401             name => "Yet Another Pluggable Bot",
402              
403             ignore_list => [qw(hitherto blech muttley)],
404              
405             );
406              
407             =head2 Running the bot (simple)
408              
409             There's a shell script installed to run the bot.
410              
411             $ bot-basicbot-pluggable --nick MyBot --server irc.perl.org
412              
413             Then connect to the IRC server, /query the bot, and set a password. See
414             L<Bot::BasicBot::Pluggable::Module::Auth> for further details.
415              
416             =head2 Running the bot (advanced)
417              
418             There are two useful ways to create a Pluggable bot. The simple way is:
419              
420             # Load some useful modules.
421             my $infobot_module = $bot->load("Infobot");
422             my $google_module = $bot->load("Google");
423             my $seen_module = $bot->load("Seen");
424              
425             # Set the Google key (see http://www.google.com/apis/).
426             $google_module->set("google_key", "some google key");
427              
428             $bot->run();
429              
430             The above lets you run a bot with a few modules, but not change those modules
431             during the run of the bot. The complex, but more flexible, way is as follows:
432              
433             # Load the Loader module.
434             $bot->load('Loader');
435              
436             # run the bot.
437             $bot->run();
438              
439             This is simpler but needs further setup once the bot is joined to a server. The
440             Loader module lets you talk to the bot in-channel and tell it to load and unload
441             other modules. The first one you'll want to load is the 'Auth' module, so that
442             other people can't load and unload modules without permission. Then you'll need
443             to log in as an admin and change the default password, per the following /query:
444              
445             !load Auth
446             !auth admin julia
447             !password julia new_password
448             !auth admin new_password
449              
450             Once you've done this, your bot is safe from other IRC users, and you can tell
451             it to load and unload other installed modules at any time. Further information
452             on module loading is in L<Bot::BasicBot::Pluggable::Module::Loader>.
453              
454             !load Seen
455             !load Google
456             !load Join
457              
458             The Join module lets you tell the bot to join and leave channels:
459              
460             <botname>, join #mychannel
461             <botname>, leave #someotherchannel
462              
463             The perldoc pages for the various modules will list other commands.
464              
465             =head1 DESCRIPTION
466              
467             Bot::BasicBot::Pluggable started as Yet Another Infobot replacement, but now
468             is a generalised framework for writing infobot-type bots that lets you keep
469             each specific function separate. You can have separate modules for factoid
470             tracking, 'seen' status, karma, googling, etc. Included default modules are
471             below. Use C<perldoc Bot::BasicBot::Pluggable::Module::<module name>> for help
472             on their individual terminology.
473              
474             Auth - user authentication and admin access.
475             DNS - host lookup (e.g. nslookup and dns).
476             Google - search Google for things.
477             Infobot - handles infobot-style factoids.
478             Join - joins and leaves channels.
479             Karma - tracks the popularity of things.
480             Loader - loads and unloads modules as bot commands.
481             Seen - tells you when people were last seen.
482             Title - gets the title of URLs mentioned in channel.
483             Vars - changes module variables.
484              
485             The way the Pluggable bot works is very simple. You create a new bot object
486             and tell it to load various modules (or, alternatively, load just the Loader
487             module and then interactively load modules via an IRC /query). The modules
488             receive events when the bot sees things happen and can, in turn, respond. See
489             C<perldoc Bot::BasicBot::Pluggable::Module> for the details of the module API.
490              
491             =head1 METHODS
492              
493             =over 4
494              
495             =item new(key => value, ...)
496              
497             Create a new Bot. Except of the additional attributes loglevel and
498             logconfig identical to the C<new> method in L<Bot::BasicBot>. Please
499             refer to their accessor for documentation.
500              
501             =item load($module)
502              
503             Load a module for the bot by name from C<./ModuleName.pm> or
504             C<./modules/ModuleName.pm> in that order if one of these files
505             exist, and falling back to C<Bot::BasicBot::Pluggable::Module::$module>
506             if not.
507              
508             =item reload($module)
509              
510             Reload the module C<$module> - equivalent to unloading it (if it's already
511             loaded) and reloading it. Will stomp the old module's namespace - warnings
512             are expected here. Not totally clean - if you're experiencing odd bugs, restart
513             the bot if possible. Works for minor bug fixes, etc.
514              
515             =item unload($module)
516              
517             Removes a module from the bot. It won't get events any more.
518              
519             =item module($module)
520              
521             Returns the handler object for the loaded module C<$module>. Used, e.g.,
522             to get the 'Auth' hander to check if a given user is authenticated.
523              
524             =item modules
525              
526             Returns a list of the names of all loaded modules as an array.
527              
528             =item available_modules
529              
530             Returns a list of all available modules whether loaded or not
531              
532             =item add_handler($handler_object, $handler_name)
533              
534             Adds a handler object with the given name to the queue of modules. There
535             is no order specified internally, so adding a module earlier does not
536             guarantee it'll get called first. Names must be unique.
537              
538             =item remove_handler($handler_name)
539              
540             Remove a handler with the given name.
541              
542             =item store
543              
544             Returns the bot's object store; see L<Bot::BasicBot::Pluggable::Store>.
545              
546             =item log
547              
548             Logs all of its argument to loglevel info. Please do not use this
549             function in new code, it's simple provided as fallback for old
550             modules.
551              
552             =item loglevel
553              
554             Returns the bots loglevel or sets it if an argument is supplied.
555             It expects trace, debug, info, warn, error or fatal as value.
556              
557             =item logconfig
558              
559             Returns the bot configuration file for logging. Please refer to
560             L<Log::Log4perl::Config> for the configurations files format. Setting
561             this to a different file after calling init() has no effect.
562              
563             Returns or set
564              
565             =item dispatch($method_name, $method_params)
566              
567             Call the named C<$method> on every loaded module with that method name.
568              
569             =item help
570              
571             Returns help for the ModuleName of message 'help ModuleName'. If no message
572             has been passed, return a list of all possible handlers to return help for.
573              
574             =item run
575              
576             Runs the bot. POE core gets control at this point; you're unlikely to get it back.
577              
578             =back
579              
580             =head1 BUGS
581              
582             During the C<make>, C<make test>, C<make install> process, POE will moan about
583             its kernel not being run. This is a C<Bot::BasicBot problem>, apparently.
584             Reloading a module causes warnings as the old module gets its namespace stomped.
585             Not a lot you can do about that. All modules must be in Bot::Pluggable::Module::
586             namespace. Well, that's not really a bug.
587              
588             =head1 REQUIREMENTS
589              
590             Bot::BasicBot::Pluggable is based on POE, and really needs the latest version.
591             Because POE is like that sometimes. You also need L<POE::Component::IRC>.
592             Oh, and L<Bot::BasicBot>. Some of the modules will need more modules, e.g.
593             Google.pm needs L<Net::Google>. See the module docs for more details.
594              
595             =head1 LICENSE
596              
597             This program is free software; you can redistribute it
598             and/or modify it under the same terms as Perl itself.
599              
600             =head1 AUTHOR
601              
602             David Precious (BIGPRESH) <davidp@preshweb.co.uk>
603              
604             I am merely the current maintainer; however, the AUTHOR heading is traditional.
605              
606             =head1 CONTRIBUTORS
607              
608             =over 2
609              
610             =item * Mario Domgoergen <mdom@cpan.org>
611              
612             =item * Tom Insam <tom@jerakeen.org>
613              
614             =item * David Precious <davidp@preshweb.co.uk>
615              
616             =item * Mike Eldridge <diz@cpan.org>
617              
618             =back
619              
620             =head1 CREDITS
621              
622             Bot::BasicBot was written initially by Mark Fowler, and worked on heavily by
623             Simon Kent, who was kind enough to apply some patches we needed for Pluggable.
624             Eventually. Oh, yeah, and I stole huge chunks of docs from the Bot::BasicBot
625             source too. I spent a lot of time in the mozbot code, and that has influenced
626             my ideas for Pluggable. Mostly to get round its awfulness.
627              
628             Various people helped with modules. Convert was almost ported from the
629             infobot code by blech. But not quite. Thanks for trying... blech has also put
630             a lot of effort into the chump.cgi & chump.tem files in the examples/ folder,
631             including some /inspired/ calendar evilness.
632              
633             And thanks to the rest of #2lmc who were my unwilling guinea pigs during
634             development. And who kept suggesting totally stupid ideas for modules that I
635             then felt compelled to go implement. Shout.pm owes its existence to #2lmc.
636              
637             Thanks to Mike Eldridge (DIZ), maintainer until 2016, for kindly transferring
638             maintainership to me, BIGPRESH, so I can keep this useful bot framework alive.
639              
640             =head1 SEE ALSO
641              
642             =over 2
643              
644             =item * L<POE>
645              
646             =item * L<POE::Component::IRC>
647              
648             =item * L<Bot::BasicBot>
649              
650             =item * Infobot: http://www.infobot.org/
651              
652             =item * Mozbot: http://www.mozilla.org/projects/mozbot/
653              
654             =back
655              
656             =cut