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