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.20';
3 10     10   50330 use warnings;
  10         13  
  10         281  
4 10     10   35 use strict;
  10         11  
  10         169  
5              
6 10     10   109 use 5.8.0;
  10         22  
7 10     10   4311 use POE;
  10         287529  
  10         54  
8 10     10   444046 use Bot::BasicBot 0.60;
  10         1296753  
  10         743  
9 10     10   6612 use Log::Log4perl;
  10         311013  
  10         56  
10 10     10   505 use Log::Log4perl::Level;
  10         14  
  10         74  
11 10     10   787 use base qw( Bot::BasicBot );
  10         12  
  10         762  
12 10     10   5279 use Data::Dumper;
  10         55365  
  10         744  
13              
14             $Data::Dumper::Terse = 1;
15             $Data::Dumper::Indent = 0;
16              
17             use Module::Pluggable
18 10         53 sub_name => '_available',
19             search_path => 'Bot::BasicBot::Pluggable::Module',
20 10     10   3914 except => 'Bot::BasicBot::Pluggable::Module::Base';
  10         64683  
21 10     10   4439 use Bot::BasicBot::Pluggable::Module;
  10         14  
  10         240  
22 10     10   3286 use Bot::BasicBot::Pluggable::Store;
  10         21  
  10         253  
23 10     10   45 use File::Spec;
  10         12  
  10         192  
24 10     10   29 use Try::Tiny;
  10         12  
  10         4153  
25              
26             sub init {
27 8     8 1 174 my $self = shift;
28 8         39 $self->init_logging();
29              
30 8         23029 my $logger = Log::Log4perl->get_logger( ref $self );
31 8         171 $logger->info( 'Starting initialization of ' . ref $self );
32              
33 8 50       60 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         13 $self->store( Bot::BasicBot::Pluggable::Store->new( $self->store ) );
54             }
55 8         23 return 1;
56             }
57              
58             sub init_logging {
59 8     8 0 10 my $self = shift;
60 8         61 my $logger = Log::Log4perl->get_logger( ref $self );
61 8 50       1882 if ( $self->logconfig ) {
62 0         0 Log::Log4perl->init( $self->logconfig );
63             }
64             else {
65 8         32 my $loglevel = $self->loglevel;
66 8         64 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 706 my $self = shift;
88 14         19 my $module = shift;
89              
90 14         53 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       256 $logger->logdie("Cannot load module without a name") unless $module;
94 14 50       50 $logger->logdie("Module $module already loaded") if $self->handler($module);
95              
96             # This is possible a leeeetle bit evil.
97 14         48 $logger->info("Loading module $module");
98 14         67 my $filename = $module;
99 14         23 $filename =~ s{::}{/}g;
100 14         28 my $file = "Bot/BasicBot/Pluggable/Module/$filename.pm";
101 14 100       208 $file = "./$filename.pm" if ( -e "./$filename.pm" );
102 14 100       106 $file = "./modules/$filename.pm" if ( -e "./modules/$filename.pm" );
103 14         91 $logger->debug("Loading module $module from file $file");
104 14         799 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   49 no warnings 'redefine';
  10         15  
  10         13150  
108 14         46 delete $INC{$file};
109              
110 14     6   147 try { require $file } catch { die "Can't load $module: $_"; };
  14         5861  
  0         0  
111              
112             # Ok, it's very evil. Don't bother me, I'm working.
113              
114 14         498 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       193 $logger->logdie( ref($m) . " isn't a $module" )
121             unless ref($m) =~ /\Q$module/;
122              
123 14         70 $self->add_handler( $m, $module );
124              
125 14         56 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 42 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         119 map { substr( ( File::Spec->splitpath($_) )[2], 0, -3 ) } glob('./*.pm'),
  2         33  
161             glob('./modules/*.pm');
162             my @central_modules =
163             map {
164 1         10 my $mod = $_;
  11         4436  
165 11         16 $mod =~ s/^Bot::BasicBot::Pluggable::Module:://;
166 11         14 $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 883 my ( $self, $name ) = @_;
175 1059         3192 return $self->{handlers}{ lc($name) };
176             }
177              
178             sub handlers {
179 961     961 0 727 my $self = shift;
180             my @keys = sort {
181 10         18 my $xa = $self->handler($a);
182 10         11 my $xb = $self->handler($b);
183             (
184 10 0 50     24 ($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         677 } keys( %{ $self->{handlers} } );
  961         2176  
189 961 50       2088 return @keys if wantarray;
190 0         0 return \@keys;
191             }
192              
193             sub add_handler {
194 14     14 1 24 my ( $self, $handler, $name ) = @_;
195 14         95 my $logger = Log::Log4perl->get_logger( ref $self );
196 14 50       355 $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       61 if $self->{handlers}{ lc($name) };
199 14         80 $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 1334     1334 1 1277 my $self = shift;
214 1334 100       1736 $self->{store_object} = shift if @_;
215 1334         2738 return $self->{store_object};
216             }
217              
218             sub loglevel {
219 11     11 1 160 my $self = shift;
220 11 100       43 $self->{loglevel} = shift if @_;
221 11   100     88 return ($self->{loglevel} and uc $self->{loglevel}) || 'WARN';
222             }
223              
224             sub logconfig {
225 11     11 1 207 my $self = shift;
226 11 100       31 $self->{logconfig} = shift if @_;
227 11         31 return $self->{logconfig};
228             }
229              
230             sub dispatch {
231 214     214 1 257 my ( $self, $method, @args ) = @_;
232 214         573 my $logger = Log::Log4perl->get_logger( ref $self );
233              
234 214         3426 $logger->info("Dispatching $method");
235 214         892 for my $who ( $self->handlers ) {
236             ## Otherwise we would see tick every five seconds
237 216 50       278 if ( $method eq 'tick' ) {
238 0         0 $logger->trace("Trying to dispatch $method to $who");
239             }
240             else {
241 216         441 $logger->debug("Trying to dispatch $method to $who");
242             }
243 216 50 33     842 $logger->trace( "... with " . Dumper(@args) )
244             if $logger->is_trace && @args;
245              
246 216 50       785 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         241 return;
258             }
259              
260             sub help {
261 1     1 1 2 my $self = shift;
262 1         2 my $mess = shift;
263 1         5 $mess->{body} =~ s/^help\s*//i;
264 1         3 my $logger = Log::Log4perl->get_logger( ref $self );
265              
266 1 50       20 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       2 if ( my $handler = $self->handler( $mess->{body} ) ) {
278             try {
279 1     1   23 return $handler->help($mess);
280             }
281             catch {
282 0     0   0 $logger->warn(
283             "Error calling help for handler $mess->{body}: $_");
284             }
285 1         6 }
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 206 my ( $self, $event, $mess ) = @_;
303 230         161 my $response;
304             my $who;
305              
306 230         553 my $logger = Log::Log4perl->get_logger( ref $self );
307 230         3547 $logger->info('Dispatching said event');
308              
309 230         984 for my $priority ( 0 .. 3 ) {
310 747         928 for my $handler ( $self->handlers ) {
311 754         537 my $response;
312 754         1710 $logger->debug(
313             "Trying to dispatch said to $handler on priority $priority");
314 754 50 33     3157 $logger->trace( '... with arguments ' . Dumper($mess) )
315             if $logger->is_trace and $mess;
316             try {
317 754     754   14563 $response =
318             $self->handler($handler)->$event( $mess, $priority );
319             }
320             catch {
321 0     0   0 $logger->warn($_);
322 754         4541 };
323 754 100 100     7355 if ( $priority and $response ) {
324 213         618 $logger->debug("Response by $handler on $priority");
325 213 50       1072 $logger->trace( 'Response is ' . Dumper($response) )
326             if $logger->is_trace;
327 213 100       855 return if $response eq '1';
328 212         276 $self->reply( $mess, $response );
329 212         400 return;
330             }
331             }
332             }
333 17         37 return;
334             }
335              
336             sub reply {
337 214     214 1 292 my ( $self, $mess, @other ) = @_;
338 214         899 $self->dispatch( 'replied', {%$mess}, @other );
339 214 50       465 if ( $mess->{reply_hook} ) {
340 214         483 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   44 my @dispatchable_events = (
349             qw/
350             connected chanjoin chanpart userquit nick_change
351             topic kicked raw_in raw_out
352             /
353             );
354 10         16 my @priority_events = (qw/ said emoted /);
355             {
356             ## no critic qw(ProhibitNoStrict)
357 10     10   120 no strict 'refs';
  10         14  
  10         810  
  10         10  
358 10         13 for my $event (@dispatchable_events) {
359             *$event = sub {
360 0     0   0 shift->dispatch( $event, @_ );
361 90         292 };
362             }
363 10         17 for my $event (@priority_events) {
364             *$event = sub {
365 230     230   344 shift->dispatch_priorities( $event, @_ );
366 20         256 };
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.20
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