File Coverage

blib/lib/Bot/IRC.pm
Criterion Covered Total %
statement 156 301 51.8
branch 38 128 29.6
condition 23 93 24.7
subroutine 31 51 60.7
pod 24 24 100.0
total 272 597 45.5


line stmt bran cond sub pod time code
1             package Bot::IRC;
2             # ABSTRACT: Yet Another IRC Bot
3              
4 13     13   553748 use 5.014;
  13         93  
5 13     13   1056 use exact;
  13         69369  
  13         91  
6              
7 13     13   20034 use Daemon::Device;
  13         153339  
  13         494  
8 13     13   6827 use Date::Format 'time2str';
  13         97407  
  13         1082  
9 13     13   123 use Encode 'decode';
  13         28  
  13         674  
10 13     13   6896 use Encode::Detect::Detector 'detect';
  13         31191  
  13         1047  
11 13     13   8740 use IO::Socket::IP -register;
  13         292605  
  13         75  
12 13     13   17828 use IO::Socket::SSL;
  13         626655  
  13         167  
13 13     13   8517 use Time::Crontab;
  13         229012  
  13         69702  
14              
15             our $VERSION = '1.38'; # VERSION
16              
17             sub new {
18 17     17 1 29426 my $class = shift;
19 17         78 my $self = bless( {@_}, $class );
20              
21 17 50       92 croak('Odd number of elements passed to new()') if ( @_ % 2 );
22             croak('connect/server not provided to new()')
23 17 100 66     419 unless ( ref $self->{connect} eq 'HASH' and $self->{connect}{server} );
24              
25 16   100     93 $self->{spawn} ||= 2;
26 16   50     101 $self->{encoding} //= 'UTF-8';
27              
28 16   100     82 $self->{connect}{nick} //= 'bot';
29 16   100     115 $self->{connect}{name} //= 'Yet Another IRC Bot';
30 16   100     85 $self->{connect}{port} ||= 6667;
31              
32 16   50 0   145 $self->{disconnect} //= sub {};
33              
34 16   100     86 $self->{daemon} //= {};
35 16   66     88 $self->{daemon}{name} //= $self->{connect}{nick};
36 16   66     99 $self->{daemon}{pid_file} //= $self->{daemon}{name} . '.pid';
37              
38 16         41 $self->{nick} = $self->{connect}{nick};
39              
40 16         44 $self->{hooks} = [];
41 16         47 $self->{ticks} = [];
42 16         41 $self->{helps} = {};
43 16         40 $self->{loaded} = {};
44 16         41 $self->{numerics} = [];
45              
46 16   50     94 $self->{send_user_nick} ||= 'on_parent';
47             croak('"send_user_nick" optional value set to invalid value') if (
48             $self->{send_user_nick} ne 'on_connect' and
49             $self->{send_user_nick} ne 'on_parent' and
50 16 50 33     131 $self->{send_user_nick} ne 'on_reply'
      33        
51             );
52              
53             $self->load(
54 2         9 ( ref $self->{plugins} eq 'ARRAY' ) ? @{ $self->{plugins} } : $self->{plugins}
55 16 50       61 ) if ( $self->{plugins} );
    100          
56              
57 15         79 return $self;
58             }
59              
60             sub run {
61 1     1 1 431 my $self = shift;
62 1         3 my $commands = \@_;
63              
64             $self->{socket} = ( ( $self->{connect}{ssl} ) ? 'IO::Socket::SSL' : 'IO::Socket::IP' )->new(
65             PeerAddr => $self->{connect}{server},
66             PeerPort => $self->{connect}{port},
67             Proto => 'tcp',
68 1 50       11 Family => ( $self->{connect}{ipv6} ? AF_INET6 : AF_INET ),
    50          
    50          
69             Type => SOCK_STREAM,
70             SSL_verify_mode => SSL_VERIFY_NONE,
71             ) or die $!;
72              
73 1 50       13 if ( $self->{encoding} ) {
74             try {
75             binmode( $self->{socket}, "encoding($self->{encoding})" );
76             }
77 1         3 catch {};
78             }
79              
80 1 50       15 if ( $self->{send_user_nick} eq 'on_connect' ) {
81 0         0 $self->{socket}->print("USER $self->{nick} 0 * :$self->{connect}{name}\r\n");
82 0         0 $self->{socket}->print("NICK $self->{nick}\r\n");
83             }
84              
85             try {
86             $self->{device} = Daemon::Device->new(
87             parent => \&_parent,
88             child => \&_child,
89             on_message => \&_on_message,
90             spawn => $self->{spawn},
91             daemon => $self->{daemon},
92             data => {
93             self => $self,
94             commands => $commands,
95             passwd => $self->{passwd},
96             },
97             );
98             }
99             catch {
100             my $e = $_ || $@;
101             croak("Daemon device instantiation failure: $e");
102 1         4 };
103              
104 1         8 $self->{device}->run;
105             }
106              
107             sub note {
108 4     4 1 10 my ( $self, $msg, $err ) = @_;
109 4         16 chomp($msg);
110 4         15 $msg = '[' . time2str( '%d/%b/%Y:%H:%M:%S %z', time() ) . '] ' . $msg . "\n";
111              
112 4 50       974 if ($err) {
113 0 0       0 die $msg if ( $err eq 'die' );
114 0         0 warn $msg;
115             }
116             else {
117 4         154 print $msg;
118             }
119              
120 4         23 return;
121             }
122              
123             sub _parent {
124 0     0   0 my ($device) = @_;
125 0         0 my $self = $device->data('self');
126 0         0 my $session = { start => time };
127             my $delegate = sub {
128             my ($random_child) =
129 0         0 map { $_->[0] }
130 0         0 sort { $a->[1] <=> $b->[1] }
131 0         0 map { [ $_, rand() ] }
132 0     0   0 @{ $device->children };
  0         0  
133              
134 0         0 $device->message( $random_child, @_ );
135 0         0 };
136             my $broadcast = sub {
137 0     0   0 my @messages = @_;
138 0         0 $device->message( $_, @messages ) for ( @{ $device->children } );
  0         0  
139 0         0 };
140              
141             local $SIG{ALRM} = sub {
142 0     0   0 alarm 1;
143 0         0 my $time = time;
144              
145 0         0 for (
146             grep {
147             ref $_->{timing} and ( $time % 60 == 0 ) and $_->{timing}->match($time) or
148 0 0 0     0 not ref $_->{timing} and ( ( $time - $session->{start} ) % $_->{timing} == 0 )
      0        
      0        
149 0         0 } @{ $self->{ticks} }
150             ) {
151             try {
152             $_->{code}->($self);
153             }
154 0         0 catch {
155             my $e = $_ || $@;
156             warn "Tick execution failure: $e\n";
157             };
158             }
159 0         0 };
160              
161 0     0   0 local $SIG{__WARN__} = sub { note( undef, $_[0], 'warn' ) };
  0         0  
162 0     0   0 local $SIG{__DIE__} = sub { note( undef, $_[0], 'die' ) };
  0         0  
163              
164 0         0 srand();
165 0         0 my @lines;
166              
167             try {
168             if ( $self->{send_user_nick} eq 'on_parent' ) {
169             $self->say("USER $self->{nick} 0 * :$self->{connect}{name}");
170             $self->say("NICK $self->{nick}");
171             }
172              
173             while ( my $line = $self->{socket}->getline ) {
174             $line =~ s/\003\d{2}(?:,\d{2})?//g; # remove IRC color codes
175             $line =~ tr/\000-\037//d; # remove all control characters
176              
177             $self->note($line);
178             chomp($line);
179              
180             if ( not $session->{established} ) {
181             if ( not $session->{user} ) {
182             if ( $self->{send_user_nick} eq 'on_reply' ) {
183             $self->say("USER $self->{nick} 0 * :$self->{connect}{name}");
184             $self->say("NICK $self->{nick}");
185             }
186             elsif ( $self->{send_user_nick} eq 'on_connect' ) {
187             $self->note("<<< USER $self->{nick} 0 * :$self->{connect}{name}\r\n");
188             $self->note("<<< NICK $self->{nick}\r\n");
189             }
190             $session->{user} = 1;
191             }
192             if ( $line =~ /^ERROR.+onnect\w+ too fast/ ) {
193             warn "$line\n";
194             warn "Sleeping 20 and retrying...\n";
195             sleep 20;
196             $device->daemon->do_restart;
197             }
198             elsif ( $line =~ /^ERROR\s/ ) {
199             warn "$line\n";
200             $device->daemon->do_stop;
201             }
202             elsif ( $line =~ /^:\S+\s433\s/ ) {
203             $self->nick( $self->{nick} . '_' );
204             }
205             elsif ( $line =~ /^:\S+\s001\s/ ) {
206             $self->say($_) for ( map {
207             my $command = $_;
208             $command =~ s|^/msg |PRIVMSG |;
209             $command =~ s|^/(\w+)|uc($1)|e;
210             $command;
211             } @{ $device->data('commands') } );
212              
213             $self->join;
214             $session->{established} = 1;
215             alarm 1 if ( @{ $self->{ticks} } );
216             }
217             }
218              
219             shift @lines while ( @lines > 10 );
220             my $now = time();
221              
222             unless ( grep { $_->{line} eq $line and $_->{time} + 1 > $now } @lines ) {
223             unless ( $line =~ /^:\S+\s\d{3}\s/ ) {
224             $delegate->($line);
225             }
226             else {
227             $broadcast->($line);
228             }
229             }
230             else {
231             $self->note("### Skipped repeated line: $line");
232             }
233              
234             push @lines, { line => $line, time => $now };
235             }
236             }
237 0         0 catch {
238             my $e = $_ || $@;
239             warn "Daemon parent loop failure: $e\n";
240             };
241              
242 0         0 kill( 'KILL', $_ ) for ( @{ $device->children } );
  0         0  
243 0 0       0 $self->{disconnect}->($self) if ( ref $self->{disconnect} eq 'CODE' );
244             }
245              
246             sub _child {
247 0     0   0 local $SIG{__WARN__} = sub { note( undef, $_[0], 'warn' ) };
  0     0   0  
248 0     0   0 local $SIG{__DIE__} = sub { note( undef, $_[0], 'die' ) };
  0         0  
249              
250 0         0 srand();
251 0         0 sleep 1 while (1);
252             }
253              
254             sub _on_message {
255 0     0   0 my $device = shift;
256 0         0 my $self = $device->data('self');
257 0         0 my $passwd = $device->data('passwd');
258              
259 0         0 for my $line (@_) {
260 0 0       0 if ( $self->{encoding} ) {
261 0         0 my $charset = detect($line);
262 0 0 0     0 $line = decode( $charset => $line ) if ( $charset and $charset eq $self->{encoding} );
263             }
264              
265 0         0 push( @{ $self->{numerics} }, $line )
266 0 0 0     0 if ( $line =~ /^:\S+\s\d{3}\s/ and @{ $self->{numerics} } < 100 );
  0         0  
267              
268 0 0       0 if ( $line =~ /^>>>\sNICK\s(.*)/ ) {
    0          
269 0         0 $self->{nick} = $1;
270 0         0 next;
271             }
272             elsif ( $line =~ /^:\S+\s433\s/ ) {
273 0         0 $self->nick( $self->{nick} . '_' );
274 0         0 next;
275             }
276              
277 0         0 $self->{in} = { map { $_ => '' } qw( line source nick user server command forum text ) };
  0         0  
278 0         0 $self->{in}{$_} = 0 for ( qw( private to_me ) );
279 0         0 $self->{in}{line} = $line;
280              
281 0 0       0 if ( $line =~ /^(ERROR)\s/ ) {
    0          
    0          
    0          
    0          
    0          
    0          
282 0         0 warn $line . "\n";
283             }
284             elsif ( $line =~ /^:(\S+?)!~?(\S+?)@(\S+?)\s(\S+)\s(\S+)\s:(.*)/ ) {
285 0         0 @{ $self->{in} }{ qw( nick user server command forum text ) } = ( $1, $2, $3, $4, $5, $6 );
  0         0  
286 0         0 $self->{in}{full_text} = $self->{in}{text};
287             }
288             elsif ( $line =~ /^:(\S+?)!~?(\S+?)@(\S+?)\s(\S+)\s:(.*)/ ) {
289 0         0 @{ $self->{in} }{ qw( nick user server command text ) } = ( $1, $2, $3, $4, $5 );
  0         0  
290             ( $self->{in}{forum} = $self->{in}{text} ) =~ s/^://
291 0 0 0     0 if ( $self->{in}{command} eq 'JOIN' or $self->{in}{command} eq 'PART' );
292             }
293             elsif ( $line =~ /^:(\S+?)!~?(\S+?)@(\S+?)\s(\S+)\s(\S+)\s(.*)/ ) {
294 0         0 @{ $self->{in} }{ qw( nick user server command forum text ) } = ( $1, $2, $3, $4, $5, $6 );
  0         0  
295             }
296             elsif ( $line =~ /^:(\S+?)!~?(\S+?)@(\S+?)\s(\S+)\s(\S+)/ ) {
297 0         0 @{ $self->{in} }{ qw( nick user server command forum ) } = ( $1, $2, $3, $4, $5, $6 );
  0         0  
298             }
299             elsif ( $line =~ /^(PING)\s(.+)/ ) {
300 0         0 @{ $self->{in} }{ qw( command text ) } = ( $1, $2 );
  0         0  
301 0         0 $self->say( 'PONG ' . $self->{in}{text} );
302 0         0 next;
303             }
304             elsif ( $line =~ /^:(\S+)\s([A-Z]+|\d+)\s(\S+)\s(.*)/ ) {
305 0         0 @{ $self->{in} }{ qw( source command forum text ) } = ( $1, $2, $3, $4 );
  0         0  
306             }
307             else {
308 0         0 warn 'Unparsed line (probably a bug in Bot::IRC; please report it): ', $line . "\n";
309             }
310              
311 0 0       0 next unless ( $self->{in}{nick} ne $self->{nick} );
312              
313 0 0       0 if ( $self->{in}{command} eq 'PRIVMSG' ) {
314 0 0 0     0 $self->{in}{private} = 1 if ( $self->{in}{forum} and $self->{in}{forum} eq $self->{nick} );
315             $self->{in}{to_me} = 1 if (
316             $self->{in}{text} =~ s/^\s*\b$self->{nick}\b[\s\W]*//i or
317             $self->{in}{text} =~ s/[\s\W]*\b$self->{nick}\b[\s\W]*$//i or
318             $self->{in}{private}
319 0 0 0     0 );
      0        
320             }
321              
322 0 0       0 if ( $self->{in}{to_me} ) {
323 0 0       0 if ( $self->{in}{text} =~ /^\s*help\W*$/i ) {
    0          
    0          
324             $self->reply_to(
325             'Ask me for help with "help topic" where the topic is one of the following: ' .
326 0         0 $self->list( ', ', 'and', sort keys %{ $self->{helps} } ) . '.'
  0         0  
327             );
328 0         0 next;
329             }
330             elsif ( $self->{in}{text} =~ /^\s*help\s+(.+?)\W*$/i ) {
331             $self->reply_to(
332 0   0     0 ( $self->{helps}{$1} || "Couldn't find the help topic: $1." )
333             );
334 0         0 next;
335             }
336             elsif ( $self->{in}{text} =~ /Sorry. I don't understand./ ) {
337 0         0 next;
338             }
339             }
340              
341 0         0 hook: for my $hook (
342 0         0 @{ $self->{hooks} },
343              
344             {
345             when => {
346             to_me => 1,
347             text => qr/^\s*cmd\s+(?<passwd>\S+)\s+(?<cmd>.+)$/i,
348             },
349             code => sub {
350 0     0   0 my ( $bot, $in, $m ) = @_;
351              
352 0 0 0     0 if ( $m->{passwd} and $passwd and $m->{passwd} eq $passwd ) {
      0        
353 0         0 $bot->say($_) for (
354             map {
355 0         0 my $command = $_;
356 0         0 $command =~ s|^/msg |PRIVMSG |;
357 0         0 $command =~ s|^/(\w+)|uc($1)|e;
  0         0  
358 0         0 $command;
359             } split( /\s*;\s*/, $m->{cmd} )
360             );
361             }
362              
363 0         0 return 1;
364             },
365             },
366              
367             {
368             when => {
369             full_text => qr/^\s*$self->{nick}\s*[!\?]\W*$/i,
370             },
371             code => sub {
372 0     0   0 my ($bot) = @_;
373 0         0 $bot->reply_to('Yes?');
374             },
375             },
376              
377             {
378             when => {
379             to_me => 1,
380             text => qr/^(?<word>hello|greetings|hi|good\s+\w+)\W*$/i,
381             },
382             code => sub {
383 0     0   0 my ( $bot, $in, $m ) = @_;
384 0         0 $bot->reply_to( ucfirst( lc( $m->{word} ) ) . '.' );
385             },
386             },
387              
388             ( map {
389             {
390             when => $_,
391             code => sub {
392 0     0   0 my ($bot) = @_;
393 0         0 $bot->reply_to(qq{Sorry. I don't understand. (Try "$self->{nick} help" for help.)});
394             },
395             },
396 0         0 } (
397             {
398             private => 0,
399             full_text => qr/^\s*$self->{nick}\s*[,:\->~=]/i,
400             },
401             {
402             private => 1,
403             },
404             ) ),
405              
406             ) {
407 0         0 my $captured_matches = {};
408              
409 0         0 for my $type ( keys %{ $hook->{when} } ) {
  0         0  
410             next hook unless (
411             ref( $hook->{when}{$type} ) eq 'Regexp' and
412             $self->{in}{$type} and $self->{in}{$type} =~ $hook->{when}{$type} or
413             ref( $hook->{when}{$type} ) eq 'CODE' and $hook->{when}{$type}->(
414             $self,
415             $self->{in}{$type},
416 0         0 { %{ $self->{in} } },
417             ) or
418             (
419             defined $self->{in}{$type} and defined $hook->{when}{$type} and
420 0 0 0     0 $self->{in}{$type} eq $hook->{when}{$type}
      0        
      0        
      0        
      0        
      0        
      0        
421             )
422             );
423              
424 13 0   13   6821 $captured_matches = { %$captured_matches, %+ } if ( keys %+ );
  13         5288  
  13         23849  
  0         0  
425             }
426              
427 0         0 my $rv;
428             try {
429             $rv = $hook->{code}->(
430             $self,
431             { %{ $self->{in} } },
432             $captured_matches,
433             );
434             }
435 0         0 catch {
436             my $e = $_ || $@;
437             warn "Plugin hook execution failure: $e\n";
438             };
439              
440 0 0       0 last if ($rv);
441             }
442             }
443             }
444              
445             sub load {
446 7     7 1 14 my $self = shift;
447              
448 7         19 for my $plugin (@_) {
449 7 50       76 unless ( ref $plugin ) {
450 7 50       42 if ( $plugin =~ /^:core$/i ) {
451 0         0 $self->load(
452             'Ping',
453             'Join',
454             'Seen',
455             'Greeting',
456             'Infobot',
457             'Functions',
458             'Convert',
459             'Karma',
460             'Math',
461             'History',
462             );
463 0         0 next;
464             }
465              
466 7         14 my $namespace;
467 7         60 for (
468             $plugin,
469             __PACKAGE__ . "::Y::$plugin",
470             __PACKAGE__ . "::X::$plugin",
471             __PACKAGE__ . "::$plugin",
472             ) {
473 22         105 ( my $path = $_ ) =~ s|::|/|g;
474              
475 22         2019 eval "require $_";
476 22 100       1457 unless ($@) {
477 6         17 $namespace = $_;
478 6         15 last;
479             }
480             else {
481 16 50       326 croak("Plugin load failure: $@") unless ( $@ =~ /^Can't locate $path/ );
482             }
483             }
484 7 100       263 croak("Unable to find or properly load $plugin") unless ($namespace);
485              
486 6 50       27 next if ( $self->{loaded}{$namespace} );
487              
488 6 50       61 $namespace->import if ( $namespace->can('import') );
489 6 50       49 croak("$namespace does not implement init()") unless ( $namespace->can('init') );
490              
491 6         473 eval "${namespace}::init(\$self)";
492 6 50       62 die("Plugin init failure: $@\n") if ($@);
493              
494 6         41 $self->{loaded}{$namespace} = time;
495             }
496             else {
497 0         0 $self->$_( @{ $plugin->{$_} } ) for ( qw( hooks ticks ) );
  0         0  
498 0         0 $self->$_( $plugin->{$_} ) for ( qw( helps subs ) );
499             }
500             }
501              
502 6         16 return $self;
503             }
504              
505             sub reload {
506 1     1 1 385 my $self = shift;
507 1         6 delete $self->{loaded}{$_} for (@_);
508 1         5 return $self->load(@_);
509             }
510              
511             sub hook {
512 3     3 1 297 my ( $self, $when, $code, $attr ) = @_;
513              
514             push(
515 3   100     5 @{ $self->{hooks} },
  3         20  
516             {
517             when => $when,
518             code => $code,
519             attr => ( $attr // {} ),
520             },
521             );
522              
523 3 50       11 $self->subs( %{ $attr->{subs} } ) if ( ref $attr->{subs} eq 'HASH' );
  0         0  
524 3 50       7 $self->helps( %{ $attr->{helps} } ) if ( ref $attr->{helps} eq 'HASH' );
  0         0  
525              
526 3         7 return $self;
527             }
528              
529             sub hooks {
530 1     1 1 280 my $self = shift;
531 1         6 $self->hook(@$_) for (@_);
532 1         2 return $self;
533             }
534              
535             sub helps {
536 10     10 1 353 my ( $self, @input ) = @_;
537              
538             try {
539             $self->{helps} = { %{ $self->{helps} }, @input };
540             }
541 10         36 catch {
542             $self->note('Plugin helps called but not properly implemented');
543             };
544              
545 10         43 return $self;
546             }
547              
548             sub tick {
549 3     3 1 276 my ( $self, $timing, $code ) = @_;
550              
551 3 50       5 push( @{ $self->{ticks} }, {
  3         24  
552             timing => ( $timing =~ /^\d+$/ ) ? $timing : Time::Crontab->new($timing),
553             code => $code,
554             } );
555 3         8 return $self;
556             }
557              
558             sub ticks {
559 1     1 1 272 my $self = shift;
560 1         5 $self->tick(@$_) for (@_);
561 1         2 return $self;
562             }
563              
564             sub subs {
565 9     9 1 288 my $self = shift;
566              
567 9 50       53 if ( @_ % 2 ) {
568 0         0 $self->note('Plugin helps called but not properly implemented');
569 0         0 return $self;
570             }
571              
572 9         28 my $subs = {@_};
573              
574 9         38 for my $name ( keys %$subs ) {
575 13     13   180 no strict 'refs';
  13         39  
  13         565  
576 13     13   85 no warnings 'redefine';
  13         29  
  13         20608  
577 10         21 *{ __PACKAGE__ . '::' . $name } = $subs->{$name};
  10         70  
578             }
579              
580 9         51 return $self;
581             }
582              
583             sub register {
584 2     2 1 284 my $self = shift;
585 2         22 $self->{loaded}{$_} = time for (@_);
586 2         8 return $self;
587             }
588              
589             sub vars {
590 4     4 1 11 my ( $self, $name ) = @_;
591 4 100       27 ( $name = lc( substr( ( caller() )[0], length(__PACKAGE__) + 2 ) ) ) =~ s/::/\-/g unless ($name);
592 4 50       28 return ( defined $self->{vars}{$name} ) ? $self->{vars}{$name} : {};
593             }
594              
595             sub settings {
596 1     1 1 3 my ( $self, $name ) = @_;
597 1 50       5 return ( defined $name ) ? $self->{$name} : { %$self };
598             }
599              
600             sub reply {
601 1     1 1 855 my ( $self, $message ) = @_;
602              
603 1 50       4 if ( $self->{in}{forum} ) {
604             $self->msg(
605 0 0       0 ( ( $self->{in}{forum} eq $self->{nick} ) ? $self->{in}{nick} : $self->{in}{forum} ),
606             $message,
607             );
608             }
609             else {
610 1         9 warn "Didn't have a target to send reply to.\n";
611             }
612 1         7 return $self;
613             }
614              
615             sub reply_to {
616 0     0 1 0 my ( $self, $message ) = @_;
617 0 0       0 return $self->reply( ( ( not $self->{in}{private} ) ? "$self->{in}{nick}: " : '' ) . $message );
618             }
619              
620             sub msg {
621 1     1 1 1665 my ( $self, $target, $message ) = @_;
622 1 50       9 $self->say( "PRIVMSG $target :" . ( ( $message =~ s/^\/me\s+// ) ? "\001ACTION $message\001" : $message ) );
623 1         5 return $self;
624             }
625              
626             sub say {
627 3     3 1 1488 my $self = shift;
628              
629 3         9 for (@_) {
630 4         9 my $string = $_;
631 4         16 $self->{socket}->print( $string . "\r\n" );
632 4         20 $self->note("<<< $string");
633             }
634 3         9 return $self;
635             }
636              
637             sub nick {
638 1     1 1 1343 my ( $self, $nick ) = @_;
639              
640 1 50       5 if ($nick) {
641 1         3 $self->{nick} = $nick;
642             $self->{device}->message( $_, ">>> NICK $self->{nick}" )
643 1         4 for ( grep { $_ != $$ } $self->{device}->ppid, @{ $self->{device}->children } );
  3         16  
  1         4  
644 1         9 $self->say("NICK $self->{nick}");
645             }
646 1         5 return $self->{nick};
647             }
648              
649             sub join {
650 0     0 1 0 my $self = shift;
651              
652 0 0 0     0 $self->say("JOIN $_") for (
    0          
653             ( not @_ and $self->{connect}{join} )
654             ? (
655             ( ref $self->{connect}{join} eq 'ARRAY' )
656 0         0 ? @{ $self->{connect}{join} }
657             : $self->{connect}{join}
658             )
659             : @_
660             );
661              
662 0         0 return $self;
663             }
664              
665             sub part {
666 0     0 1 0 my $self = shift;
667 0         0 $self->say("PART $_") for (@_);
668 0         0 return $self;
669             }
670              
671             sub list {
672 2     2 1 785 my ( $self, $separator, $conjunction ) = ( shift, shift, shift );
673 2         5 my @list = @_;
674              
675 2 100       10 if ( @list > 2 ) {
    50          
676 1         11 return CORE::join( $separator, @list[ 0 .. @list - 2 ], $conjunction . ' ' . $list[-1] );
677             }
678             elsif ( @list > 1 ) {
679 1         8 return $list[0] . ' ' . $conjunction . ' ' . $list[1];
680             }
681             else {
682 0           return $list[0];
683             }
684             }
685              
686             sub health {
687 0     0 1   my ($self) = @_;
688              
689             return {
690             nick => $self->{nick},
691             server => $self->{connect}{server},
692             port => $self->{connect}{port},
693             ssl => $self->{connect}{ssl},
694             spawn => $self->{spawn},
695 0           hooks => scalar( @{ $self->{hooks} } ),
696 0           ticks => scalar( @{ $self->{ticks} } ),
697 0           plugins => scalar( keys %{ $self->{loaded} } ),
  0            
698             };
699             }
700              
701             sub numerics {
702 0     0 1   my ($self) = @_;
703 0           return $self->{numerics};
704             }
705              
706             1;
707              
708             __END__
709              
710             =pod
711              
712             =encoding UTF-8
713              
714             =head1 NAME
715              
716             Bot::IRC - Yet Another IRC Bot
717              
718             =head1 VERSION
719              
720             version 1.38
721              
722             =for markdown [![test](https://github.com/gryphonshafer/Bot-IRC/workflows/test/badge.svg)](https://github.com/gryphonshafer/Bot-IRC/actions?query=workflow%3Atest)
723             [![codecov](https://codecov.io/gh/gryphonshafer/Bot-IRC/graph/badge.svg)](https://codecov.io/gh/gryphonshafer/Bot-IRC)
724              
725             =head1 SYNOPSIS
726              
727             use Bot::IRC;
728              
729             # minimal bot instance that does basically nothing except join a channel
730             Bot::IRC->new(
731             connect => {
732             server => 'irc.perl.org',
733             join => '#test',
734             },
735             )->run;
736              
737             # illustrative example of most settings and various ways to get at them
738             my $bot = Bot::IRC->new(
739             spawn => 2,
740             daemon => {
741             name => 'bot',
742             lsb_sdesc => 'Yet Another IRC Bot',
743             pid_file => 'bot.pid',
744             stderr_file => 'bot.err',
745             stdout_file => 'bot.log',
746             },
747             connect => {
748             server => 'irc.perl.org',
749             port => '6667',
750             nick => 'yabot',
751             name => 'Yet Another IRC Bot',
752             join => [ '#test', '#perl' ],
753             ssl => 0,
754             ipv6 => 0,
755             },
756             plugins => [
757             ':core',
758             ],
759             vars => {
760             store => 'bot.yaml',
761             },
762             send_user_nick => 'on_parent', # or 'on_connect' or 'on_reply'
763             );
764              
765             $bot->load( 'Infobot', 'Karma' );
766              
767             ## Example inline plugin structure
768             # $bot->load({
769             # hooks => [ [ {}, sub {}, {} ] ],
770             # helps => { name => 'String' },
771             # subs => { name => sub {} },
772             # ticks => [ [ '0 * * * *', sub {} ] ],
773             # });
774              
775             $bot->run;
776              
777             =head1 DESCRIPTION
778              
779             Yet another IRC bot. Why? There are so many good bots and bot frameworks to
780             select from, but I wanted a bot framework that worked like a Unix service
781             out-of-the-box, operated in a pre-fork way to serve multiple concurrent
782             requests, and has a dirt-simple and highly extendable plugin mechanism. I also
783             wanted to keep the direct dependencies and core bot minimalistic, allowing as
784             much functionality as possible to be defined as optional plugins.
785              
786             =head2 Minimal Bot
787              
788             You can have a running IRC bot with as little as:
789              
790             use Bot::IRC;
791              
792             Bot::IRC->new(
793             connect => {
794             server => 'irc.perl.org',
795             },
796             )->run;
797              
798             This won't actually do much apart from connecting to the server and responding
799             to pings, but it's useful to understand how this works. Let's say you place the
800             above code into a "bot.pl" file. You start the bot with:
801              
802             ./bot.pl start
803              
804             This will startup the bot. Command-line commands include: start, stop, restart,
805             reload, status, help, and so on. (See L<Daemon::Control> for more details.)
806              
807             =head2 Pre-Forking Device
808              
809             When the bot is started, the parent process will fork or spawn a given number
810             of children workers. You can control their number along with setting locations
811             for things like PID file, log files, and so on.
812              
813             Bot::IRC->new(
814             spawn => 2,
815             daemon => {
816             name => 'bot',
817             lsb_sdesc => 'Yet Another IRC Bot',
818             pid_file => 'bot.pid',
819             stderr_file => 'bot.err',
820             stdout_file => 'bot.log',
821             },
822             )->run;
823              
824             (See L<Daemon::Device> for more details.)
825              
826             =head1 MAIN METHODS
827              
828             The following are the main or primary available methods from this class.
829              
830             =head2 new
831              
832             This method instantiates a bot object that's potentially ready to start running.
833             All bot settings can be specified to the C<new()> constructor, but some can be
834             set or added to through other methods off the instantiated object.
835              
836             Bot::IRC->new(
837             spawn => 2,
838             daemon => {},
839             connect => {
840             server => 'irc.perl.org',
841             port => '6667',
842             nick => 'yabot',
843             name => 'Yet Another IRC Bot',
844             join => [ '#test', '#perl' ],
845             ssl => 0,
846             ipv6 => 0,
847             },
848             plugins => [],
849             vars => {},
850             )->run;
851              
852             C<spawn> will default to 2. Under C<connect>, C<port> will default to 6667.
853             C<join> can be either a string or an arrayref of strings representing channels
854             to join after connnecting. C<ssl> is a true/false setting for whether to
855             connect to the server over SSL. C<ipv6> is also true/false setting for whether
856             to forcibly connect to the server over IPv6.
857              
858             You can optionally also provide an C<encoding> string representing a strict name
859             of an encoding standard. If you don't set this, it will default to "UTF-8"
860             internally. The encoding string is used to set the binmode for log files and for
861             message text decoding as necessary. If you want to turn off this functionality,
862             set C<encoding> to any defined false value.
863              
864             Read more about plugins below for more information about C<plugins> and C<vars>.
865             Consult L<Daemon::Device> and L<Daemon::Control> for more details about C<spawn>
866             and C<daemon>.
867              
868             There's also an optional C<send_user_nick> parameter, which you probably won't
869             need to use, which defines when the bot will send the C<USER> and initial
870             C<NICK> commands to the IRC server. There are 3 options: C<on_connect>,
871             C<on_parent> (the default), and C<on_reply>. C<on_connect> sends the C<USER>
872             and initial C<NICK> immediately upon establishing a connection to the IRC
873             server, prior to the parent runtime loop and prior to children creation.
874             C<on_parent> (the default) sends the 2 commands within the parent runtime loop
875             prior to any responses from the IRC server. C<on_reply> (the only option in
876             versions <= 1.23 of this module) sends the 2 commands after the IRC server
877             replies with some sort of content after connection.
878              
879             If you provide a C<disconnect> value as a reference to a subroutine, it will be
880             called when the bot is disconnected from a host. It's important to keep in mind
881             that this code is run from within the parent of the daemon, not your program, so
882             it's context will be different. The intent of this bot's design is to run as a
883             service, not a program. This hook is provided so the parent process can send a
884             signal to something that might want to take action.
885              
886             =head2 run
887              
888             This should be the last call you make, which will cause your program to operate
889             like a Unix service from the command-line. (See L<Daemon::Control> for
890             additional details.)
891              
892             C<run> can optionally be passed a list of strings that will be executed after
893             connection to the IRC server. These should be string commands similar to what
894             you'd type in an IRC client. For example:
895              
896             Bot::IRC->new( connect => { server => 'irc.perl.org' } )->run(
897             '/msg nickserv identify bot_password',
898             '/msg operserv identify bot_password',
899             '/oper bot_username bot_password',
900             '/msg chanserv identify #bot_talk bot_password',
901             '/join #bot_talk',
902             '/msg chanserv op #bot_talk',
903             );
904              
905             =head1 PLUGINS
906              
907             To do anything useful with a bot, you have to load plugins. You can do this
908             either by specifying a list of plugins with the C<plugins> key passed to
909             C<new()> or by calling C<load()>.
910              
911             Plugins are just simple packages (or optionally a hashref, but more on that
912             later). The only requirement for plugins is that they provide an C<init()>
913             method. This will get called by the bot prior to forking its worker children.
914             It will be passed the bot object. Within C<init()>, you can call any number of
915             plugin methods (see the list of methods below) to setup desired functionality.
916              
917             package Your::Plugin;
918             use strict;
919             use warnings;
920              
921             sub init {
922             my ($bot) = @_;
923              
924             $bot->hook(
925             {
926             to_me => 1,
927             text => qr/\b(?<word>w00t|[l1][e3]{2}[t7])\b/i,
928             },
929             sub {
930             my ( $bot, $in, $m ) = @_;
931             $bot->reply("$in->{nick}, don't use the word: $m->{word}.");
932             },
933             );
934             }
935              
936             1;
937              
938             When you load plugins, you can specify their packages a few different ways. When
939             attempting to load a plugin, the bot will start by looking for the name you
940             provided as a sub-class of itself. Then it will look for the plugin under the
941             assumption you provided it's full name.
942              
943             plugins => [
944             'Store', # matches "Bot::IRC::Store"
945             'Random', # matches "Bot::IRC::X::Random"
946             'Thing', # matches "Bot::IRC::Y::Thing"
947             'My::Own::Plugin', # matches "My::Own::Plugin"
948             ],
949              
950             An unenforced convention for public/shared plugins is to have non-core plugins
951             (all plugins not provided directly by this CPAN library) subclasses of
952             "Bot::IRC::X". For private/unshared plugins, you can specify whatever name you
953             want, but maybe consider something like "Bot::IRC::Y". Plugins set in the X or
954             Y subclass namespaces will get matched just like core plugins. "Y" plugins will
955             have precedence over "X" which in turn will have precedence over core.
956              
957             If you need to allow for variables to get passed to your plugins, an unenforced
958             convention is to do so via the C<vars> key to C<new()>.
959              
960             =head2 Core Plugins
961              
962             If you specify ":core" as a plugin name, it will be expanded to load all the
963             core plugins. Core plugins are all the plugins that are bundled and
964             distributed with L<Bot::IRC>.
965              
966             =over 4
967              
968             =item *
969              
970             L<Bot::IRC::Ping>
971              
972             =item *
973              
974             L<Bot::IRC::Join>
975              
976             =item *
977              
978             L<Bot::IRC::Seen>
979              
980             =item *
981              
982             L<Bot::IRC::Greeting>
983              
984             =item *
985              
986             L<Bot::IRC::Infobot>
987              
988             =item *
989              
990             L<Bot::IRC::Functions>
991              
992             =item *
993              
994             L<Bot::IRC::Convert>
995              
996             =item *
997              
998             L<Bot::IRC::Karma>
999              
1000             =item *
1001              
1002             L<Bot::IRC::Math>
1003              
1004             =item *
1005              
1006             L<Bot::IRC::History>
1007              
1008             =back
1009              
1010             Some core plugins require a storage plugin. If you don't specify one in your
1011             plugins list, then the default L<Bot::IRC::Store> will be used, which is
1012             probably not what you want (for performance reasons). Try
1013             L<Bot::IRC::Store::SQLite> instead.
1014              
1015             plugins => [
1016             'Store::SQLite',
1017             ':core',
1018             ],
1019              
1020             =head1 PLUGIN METHODS
1021              
1022             The following are methods available from this class related to plugins.
1023              
1024             =head2 load
1025              
1026             This method loads plugins. It is the exact equivalent of passing strings to the
1027             C<plugins> key in C<new()>. If a plugin has already been loaded, it'll get
1028             skipped.
1029              
1030             my $bot = Bot::IRC->new(
1031             connect => { server => 'irc.perl.org' },
1032             plugins => [ 'Store', 'Infobot', 'Karma' ],
1033             );
1034              
1035             $bot->load( 'Infobot', 'Seen' );
1036              
1037             From within your plugins, you can call C<load()> to specify plugin dependencies
1038             in your plugins.
1039              
1040             sub init {
1041             my ($bot) = @_;
1042             $bot->load('Dependency');
1043             }
1044              
1045             =head2 reload
1046              
1047             If you need to actually reload a plugin, call C<reload>. It operates in the same
1048             was as C<load>, only it won't skip already-loaded plugins.
1049              
1050             =head2 hook
1051              
1052             This is the method you'll call to add a hook, which is basically a message
1053             response handler. A hook includes a conditions trigger, some code to run
1054             when the trigger fires, and an optional additional attributes hashref.
1055              
1056             $bot->hook(
1057             {
1058             to_me => 1,
1059             text => qr/\b(?<word>w00t|[l1][e3]{2}[t7])\b/i,
1060             },
1061             sub {
1062             my ( $bot, $in, $m ) = @_;
1063             $bot->reply("$in->{nick}, don't use the word: $m->{word}.");
1064             },
1065             {
1066             subs => [],
1067             helps => [],
1068             },
1069             );
1070              
1071             The conditions trigger is a hashref of key-value pairs where the key is a
1072             component of the message and the value is either a value to exact match or a
1073             regular expression to match.
1074              
1075             The code block will receive a copy of the bot, a hashref of key-value pairs
1076             representing the message the hook is responding to, and an optionally-available
1077             hashref of any named matches from the regexes in the trigger.
1078              
1079             The hashref representing the message the hook will have the following keys:
1080              
1081             =over 4
1082              
1083             =item *
1084              
1085             C<text>: text component of the message
1086              
1087             =item *
1088              
1089             C<command>: IRC "command" like PRIVMSG, MODE, etc.
1090              
1091             =item *
1092              
1093             C<forum>: origin location like #channel or the nick who privately messaged
1094              
1095             =item *
1096              
1097             C<private>: 1 or 0 representing if the message is private or in a channel
1098              
1099             =item *
1100              
1101             C<to_me>: 1 or 0 representing if the message is addressing the bot or not
1102              
1103             =item *
1104              
1105             C<nick>: nick of the sender of the message
1106              
1107             =item *
1108              
1109             C<source>: the source server's label/name
1110              
1111             =item *
1112              
1113             C<user>: username of the sender of the message
1114              
1115             =item *
1116              
1117             C<server>: server of the sender of the message
1118              
1119             =item *
1120              
1121             C<line>: full message line/text
1122              
1123             =item *
1124              
1125             C<full_text>: text component of the message with nick included
1126              
1127             =back
1128              
1129             B<The return value from the code block is important.> If you return a positive
1130             value, all additional hooks are skipped because it will be assumed that this
1131             hook properly responded to the message and no additional work needs to be done.
1132             If the code block returns a false value, additional hooks will be checked as if
1133             this hook's trigger caused the code block to be skipped.
1134              
1135             The optional additional attributes hashref supports a handful of keys.
1136             You can specify C<subs> and C<helps>, which are exactly equivalent to
1137             calling C<subs()> and C<helps()>. (See below.)
1138              
1139             =head2 hooks
1140              
1141             This method accepts a list of arrayrefs, each containing a trigger, code, and
1142             attribute value and calls C<hook> for each set.
1143              
1144             ## Example hooks call structure
1145             # $bot->hooks(
1146             # [ {}, sub {}, {} ],
1147             # [ {}, sub {}, {} ],
1148             # );
1149              
1150             =head2 helps
1151              
1152             This method is how you'd setup any help text you'd like the bot to provide to
1153             users. It expects some number of key-value pairs where the key is the topic
1154             title of the set of functionality and the value is the string of instructions.
1155              
1156             $bot->helps(
1157             seen => 'Tracks when and where people were seen. Usage: seen <nick>, hide, unhide.',
1158             join => 'Join and leave channels. Usage: join <channel>, leave <channel>, channels.',
1159             );
1160              
1161             In the example above, let's say your bot had the nick of "bot" and you were in
1162             the same channel as your bot and you typed "bot, help" in your IRC channel. The
1163             bot would respond with a list of available topics. Then if you typed "bot, help
1164             seen" in the channel, the bot would reply with the "seen" string of
1165             instructions. If typing directly to the bot (in a private message directly to
1166             the bot), you don't need to specify the bot's name.
1167              
1168             =head2 tick
1169              
1170             Sometimes you'll want the bot to do something at a specific time or at some sort
1171             of interval. You can cause this to happen by filing ticks. A tick is similar to
1172             a hook in that it's a bit of code that gets called, but not based on a message
1173             but based on time. C<tick()> expects two values. The first is either an integer
1174             representing the number of seconds of interval between calls to the code or a
1175             crontab-like time expression. The second value is the code to call, which will
1176             receive a copy of the bot object.
1177              
1178             $bot->tick(
1179             10,
1180             sub {
1181             my ($bot) = @_;
1182             $bot->msg( '#test', '10-second interval.' );
1183             },
1184             );
1185              
1186             $bot->tick(
1187             '0 0 * * *',
1188             sub {
1189             my ($bot) = @_;
1190             $bot->msg( '#test', "It's midnight!" );
1191             },
1192             );
1193              
1194             =head2 ticks
1195              
1196             This method accepts a list of arrayrefs, each containing a time value and code
1197             block and calls C<tick> for each set.
1198              
1199             $bot->ticks(
1200             [ 10, sub {} ],
1201             [ '0 0 * * *', sub {} ],
1202             );
1203              
1204             =head2 subs
1205              
1206             A plugin can also provide functionality to the bot for use in other plugins.
1207             It can also override core methods of the bot. You do this with the C<subs()>
1208             method.
1209              
1210             $bot->subs(
1211             incr => sub {
1212             my ( $bot, $int ) = @_;
1213             return ++$int;
1214             },
1215             );
1216              
1217             my $value = $bot->incr(42); # value is 43
1218              
1219             =head2 register
1220              
1221             There are rare cases when you're writing your plugin where you want to claim
1222             that your plugin satisfies the requirements for a different plugin. In other
1223             words, you want to prevent the future loading of a specific plugin or plugins.
1224             You can do this by calling C<register()> with the list of plugins (by full
1225             namespace) that you want to skip.
1226              
1227             $bot->register('Bot::IRC::Storage');
1228              
1229             Note that this will not block the reloading of plugins with C<reload()>.
1230              
1231             =head2 vars
1232              
1233             When you are within a plugin, you can call C<vars()> to get the variables for
1234             the plugin by it's lower-case "simplified" name, which is the plugin's class
1235             name all lower-case, without the preceding "Bot::IRC::" bit, and with "::"s
1236             replaced with dashes. For example, let's say you were writing a
1237             "Bot::IRC::X::Something" plugin. You would have users set variables in their
1238             instantiation like so:
1239              
1240             Bot::IRC->new
1241             plugins => ['Something'],
1242             vars => { x-something => { answer => 42 } },
1243             )->run;
1244              
1245             Then from within the "Bot::IRC::X::Something" plugin, you would access these
1246             variables like so:
1247              
1248             my $my_vars = $bot->vars;
1249             say 'The answer to life, the universe, and everything is ' . $my_vars->{answer};
1250              
1251             If you want to access the variables for a different namespace, pass into
1252             C<vars()> the "simplified" name you want to access.
1253              
1254             my $my_other_vars = $bot->vars('x-something-else');
1255              
1256             =head2 settings
1257              
1258             If you need access to the bot's settings, you can do so with C<settings()>.
1259             Supply the setting name/key to get that setting, or provide no name/key to get
1260             all settings as a hashref.
1261              
1262             my $connection_settings_hashref = $bot->settings('connect');
1263              
1264             =head1 INLINE PLUGINS
1265              
1266             You can optionally inject inline plugins by providing them as hashref. This
1267             works both with C<load()> and the C<plugins> key.
1268              
1269             ## Example inline plugin structure
1270             # $bot->load(
1271             # {
1272             # hooks => [ [ {}, sub {}, {} ], [ {}, sub {}, {} ] ],
1273             # ticks => [ [ 10, sub {} ], [ '0 0 * * *', sub {} ] ],
1274             # helps => { title => 'Description.' },
1275             # subs => { name => sub {} },
1276             # },
1277             # {
1278             # hooks => [ [ {}, sub {}, {} ], [ {}, sub {}, {} ] ],
1279             # ticks => [ [ 10, sub {} ], [ '0 0 * * *', sub {} ] ],
1280             # helps => { title => 'Description.' },
1281             # subs => { name => sub {} },
1282             # },
1283             # );
1284              
1285             =head1 OPERATIONAL METHODS
1286              
1287             The following are operational methods available from this class, expected to be
1288             used inside various code blocks passed to plugin methds.
1289              
1290             =head2 reply
1291              
1292             If you're inside a hook, you can usually respond to most messages with the
1293             C<reply()> method, which accepts the text the bot should reply with. The method
1294             returns the bot object.
1295              
1296             $bot->reply('This is a reply. Impressive, huh?');
1297              
1298             If you want to emote something back or use any other IRC command, type it just
1299             as you would in your IRC client.
1300              
1301             $bot->reply('/me feels something, which for a bot is rather impressive.');
1302              
1303             =head2 reply_to
1304              
1305             C<reply_to> is exactly like C<reply> except that if the forum for the reply is
1306             a channel instead of to a specific person, the bot will prepend the message
1307             by addressing the nick who was the source of the response the bot is responding
1308             to.
1309              
1310             =head2 msg
1311              
1312             Use C<msg()> when you don't have a forum to reply to or want to reply in a
1313             different forum (i.e. to a different user or channel). The method accepts the
1314             forum for the message and the message text.
1315              
1316             $bot->msg( '#test', 'This is a message for everybody in #test.');
1317              
1318             =head2 say
1319              
1320             Use C<say()> to write low-level lines to the IRC server. The method expects a
1321             string that's a properly IRC message.
1322              
1323             $bot->say('JOIN #help');
1324             $bot->say('PRIVMSG #help :I need some help.');
1325              
1326             =head2 nick
1327              
1328             Use C<nick> to change the bot's nick. If the nick is already in use, the bot
1329             will try appending "_" to it until it finds an open nick.
1330              
1331             =head2 join
1332              
1333             Use C<join()> to join channels.
1334              
1335             $bot->join('#help');
1336              
1337             If some sort of persistent storage plugin is loaded, the bot will remember the
1338             channels it has joined or parted and use that as it's initial join on restart.
1339              
1340             =head2 part
1341              
1342             Use C<part()> to part channels.
1343              
1344             $bot->part('#help');
1345              
1346             If some sort of persistent storage plugin is loaded, the bot will remember the
1347             channels it has joined or parted and use that as it's initial join on restart.
1348              
1349             =head1 RANDOM HELPFUL METHODS
1350              
1351             The following are random additional methods that might be helpful in your
1352             plugins.
1353              
1354             =head2 list
1355              
1356             This method is a simple string method that takes a list and crafts it for
1357             readability. It expects a separator string, a final item conjunction string,
1358             and a list of items.
1359              
1360             $bot->list( ', ', 'and', 'Alpha', 'Beta', 'Delta', 'Gamma' );
1361             # returns "Alpha, Beta, Delta, and Gamma"
1362              
1363             $bot->list( ', ', 'and', 'Alpha', 'Beta' );
1364             # returns "Alpha and Beta"
1365              
1366             =head2 health
1367              
1368             This method returns a hashref of simple key value pairs for different "health"
1369             aspects (or current state) of the bot. It includes things like server and port
1370             connection, number of children, and so on.
1371              
1372             =head2 note
1373              
1374             While in theory you shouldn't ever need to use it, there is a method called
1375             "note" which is a handler for writing to the log and error files. If you
1376             C<warn> or C<die>, this handler steps in automatically. If you'd like to
1377             C<print> to STDOUT, which you really shouldn't need to do, then it's best to
1378             call this method instead. The reason being is that the log file is designed to
1379             be parsed in a specific way. If you write whatever you want to it, it will
1380             corrupt the log file. That said, if you really, really want to, here's how you
1381             use C<note>:
1382              
1383             $bot->note('Message'); # writes a message to the log file
1384             $bot->note( 'Message', 'warn' ); # writes a message to the error file
1385             $bot->note( 'Message', 'die' ); # writes a message to the error file the dies
1386              
1387             =head2 numerics
1388              
1389             This method will return an arrayref of scalar strings, each an IRC numeric line
1390             from the server. The arrayref is limited to the first 100 numerics from the
1391             server.
1392              
1393             =head1 SEE ALSO
1394              
1395             You can look for additional information at:
1396              
1397             =over 4
1398              
1399             =item *
1400              
1401             L<GitHub|https://github.com/gryphonshafer/Bot-IRC>
1402              
1403             =item *
1404              
1405             L<MetaCPAN|https://metacpan.org/pod/Bot::IRC>
1406              
1407             =item *
1408              
1409             L<GitHub Actions|https://github.com/gryphonshafer/Bot-IRC/actions>
1410              
1411             =item *
1412              
1413             L<Codecov|https://codecov.io/gh/gryphonshafer/Bot-IRC>
1414              
1415             =item *
1416              
1417             L<CPANTS|http://cpants.cpanauthors.org/dist/Bot-IRC>
1418              
1419             =item *
1420              
1421             L<CPAN Testers|http://www.cpantesters.org/distro/T/Bot-IRC.html>
1422              
1423             =back
1424              
1425             =head1 AUTHOR
1426              
1427             Gryphon Shafer <gryphon@cpan.org>
1428              
1429             =head1 COPYRIGHT AND LICENSE
1430              
1431             This software is Copyright (c) 2016-2021 by Gryphon Shafer.
1432              
1433             This is free software, licensed under:
1434              
1435             The Artistic License 2.0 (GPL Compatible)
1436              
1437             =cut