File Coverage

blib/lib/IRC/Indexer/Trawl/Bot.pm
Criterion Covered Total %
statement 102 215 47.4
branch 23 82 28.0
condition 12 51 23.5
subroutine 23 42 54.7
pod 9 30 30.0
total 169 420 40.2


line stmt bran cond sub pod time code
1             package IRC::Indexer::Trawl::Bot;
2              
3 2     2   213074 use 5.10.1;
  2         7  
  2         84  
4 2     2   10 use strict;
  2         4  
  2         66  
5 2     2   11 use warnings;
  2         2  
  2         61  
6 2     2   11 use Carp;
  2         7  
  2         149  
7              
8 2     2   427 use IRC::Indexer;
  2         5  
  2         51  
9              
10 2     2   11492 use IRC::Indexer::Report::Server;
  2         7  
  2         80  
11              
12 2     2   3304 use POE;
  2         112050  
  2         24  
13 2     2   228806 use POE::Component::IRC;
  2         526610  
  2         102  
14 2     2   2840 use POE::Component::IRC::Plugin::CTCP;
  2         3315  
  2         76  
15              
16 2         6892 use IRC::Utils qw/
17             decode_irc
18             strip_color strip_formatting
19 2     2   14 /;
  2         5  
20              
21             sub new {
22 2     2 1 857 my $class = shift;
23 2         6 my $self = {};
24 2         7 bless $self, $class;
25              
26 2         13 $self->{State} = {};
27            
28 2         10 my %args = @_;
29 2         23 $args{lc $_} = delete $args{$_} for keys %args;
30              
31 2   50     23 $self->verbose($args{verbose} || 0);
32              
33 2   100     23 $self->{timeout} = $args{timeout} || 90;
34 2   50     14 $self->{interval} = $args{interval} || 5;
35              
36 2   33     11 $self->{ircserver} = $args{server}
37             || croak "No Server specified in new" ;
38              
39 2   50     14 $self->{ircport} = $args{port} || 6667 ;
40 2   33     60 $self->{ircnick} = $args{nickname} || 'iindx'.(int rand 666);
41            
42 2 50       11 $self->{bindaddr} = $args{bindaddr} if $args{bindaddr};
43 2   50     16 $self->{useipv6} = $args{ipv6} || 0;
44              
45 2 100 66     22 $self->{POST} = delete $args{postback}
46             if $args{postback} and ref $args{postback};
47              
48 2         21 $self->{Serv} = IRC::Indexer::Report::Server->new;
49              
50 2         9 return $self
51             }
52              
53 1     1 1 7 sub trawler_for { return $_[0]->{ircserver} }
54              
55             sub spawn {
56 0     0 1 0 my ($pkg, %opts) = @_;
57 0 0       0 croak "cannot use spawn() interface without a postback"
58             unless $opts{postback};
59 0         0 my $self = $pkg->new(%opts);
60 0         0 $self->run();
61 0         0 return $self->{sessid}
62             }
63              
64             sub run {
65 1     1 1 729 my ($self) = @_;
66              
67 1         7 $self->{Serv}->connectedto( $self->{ircserver} );
68            
69 1         28 my $sess = POE::Session->create(
70             object_states => [
71             $self => [
72            
73             ## Internals / PoCo::IRC:
74             qw/
75             _start
76             _stop
77             shutdown
78            
79             b_check_timeout
80             b_retrieve_info
81             b_issue_cmd
82            
83             irc_connected
84             irc_001
85            
86             irc_disconnected
87             irc_error
88             irc_socketerr
89             /,
90            
91             ## Numerics:
92             ## MOTD
93             'irc_372',
94             'irc_375',
95             'irc_376',
96             ## LINKS
97             'irc_364',
98             'irc_365',
99             ## LUSERS
100             'irc_251',
101             'irc_252',
102             ## LIST
103             'irc_322',
104             'irc_323',
105             ] ],
106             );
107            
108 1         309 $self->{sessid} = $sess->ID;
109              
110 1         13 $self->{Serv}->startedat( time() );
111            
112 1         8 return $self
113             }
114              
115             sub verbose {
116 6     6 0 14 my ($self, $verbose) = @_;
117 6 100       28 return $self->{verbose} = $verbose if defined $verbose;
118 4         16 return $verbose
119             }
120              
121             sub irc {
122 6     6 0 13 my ($self, $irc) = @_;
123 6 100       20 return $self->{ircobj} = $irc if $irc;
124 3         61 return $self->{ircobj}
125             }
126              
127 0     0 0 0 sub info { report(@_) }
128             sub report {
129 23     23 1 34 my ($self) = @_;
130 23         182 return $self->{Serv}
131             }
132              
133             ## Status accessors
134              
135             sub failed {
136 2     2 1 9 my ($self, $reason) = @_;
137 2 50       8 return unless ref $self->report;
138            
139 2 100       9 if ($reason) {
140 1 50       6 carp "Trawl run failed: $reason" if $self->verbose;
141 1         4 $self->report->status('FAIL');
142 1         4 $self->report->failed($reason);
143 1         4 $self->report->finishedat(time);
144             } else {
145 1 50 33     3 return unless defined $self->report->status
146             and $self->report->status eq 'FAIL';
147             }
148 2         6 return $self->report->failed
149             }
150              
151             sub done {
152 3     3 1 3315 my ($self, $finished) = @_;
153            
154 3 50       11 if ($finished) {
155 0 0       0 carp "Trawler completed: ".$self->report->connectedto
156             if $self->verbose;
157 0         0 $self->report->status('DONE');
158 0         0 $self->report->finishedat(time());
159             }
160              
161 3 50       115 return unless ref $self->report;
162 3 50 33     8 return unless $self->report->status eq 'DONE'
163             or $self->report->status eq 'FAIL';
164 3         11 return $self->report->status
165             }
166              
167             sub dump {
168 0     0 1 0 my ($self) = @_;
169             ## return() if we're not done:
170 0 0       0 return unless ref $self->report;
171 0 0 0     0 return unless defined $self->report->status
      0        
172             and $self->report->status eq 'DONE'
173             or $self->report->status eq 'FAIL';
174             ## else return hashref of net info (or failure status)
175             ## that way masters can iterate through a pool of bots and check 'em
176             ## frontends can serialize / store
177 0         0 return $self->report->netinfo
178             }
179              
180             sub ID {
181             ## Get our POE SessionID if running.
182 2     2 1 4 my ($self) = @_;
183 2         14 return $self->{sessid}
184             }
185              
186 1     1   141 sub _stop {}
187             sub shutdown {
188 2     2 0 3037 my ($self, $kernel) = @_[OBJECT, KERNEL];
189            
190 2         11 $kernel->alarm('b_check_timeout');
191 2         217 $kernel->alarm('b_issue_cmd');
192              
193 2 50       108 warn "-> Trawler shutdown called\n" if $self->verbose;
194              
195 2 50       8 $self->done(1) unless $self->done;
196 2 100       8 $self->irc->yield('shutdown', "Leaving", 2) if ref $self->irc;
197 2         108 $self->irc(1);
198            
199 2 100       11 if (my $postback = delete $self->{POST}) {
200 1         175 $postback->($self);
201             }
202             }
203              
204             sub _start {
205 1     1   454 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
206            
207 1         13 my %ircopts = (
208             nick => $self->{ircnick},
209             username => 'ircindexer',
210             ircname => __PACKAGE__,
211             server => $self->{ircserver},
212             port => $self->{ircport},
213             useipv6 => $self->{useipv6},
214             );
215 1 50       4 $ircopts{localaddr} = $self->{bindaddr} if $self->{bindaddr};
216            
217 1         20 my $irc = POE::Component::IRC->spawn( %ircopts );
218 1         5386 $self->irc( $irc );
219              
220 1 50       4 warn "-> Trawler spawned IRC\n" if $self->verbose;
221            
222 1         14 $irc->plugin_add('CTCP' =>
223             POE::Component::IRC::Plugin::CTCP->new(
224             version => __PACKAGE__.' '.$IRC::Indexer::VERSION,
225             ),
226             );
227            
228 1         229 $irc->yield(register => qw/
229             connected
230             disconnected
231             socketerr
232             error
233            
234             001
235            
236             375 372 376
237              
238             364 365
239            
240             251 252
241            
242             322 323
243             / );
244              
245 1         81 $irc->yield(connect => {});
246            
247 1         85 $kernel->alarm( 'b_check_timeout', time + 5 );
248             }
249              
250             sub b_retrieve_info {
251 0     0 0 0 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
252             ## called via alarm() (in irc_001)
253              
254 0 0       0 warn "-> Retrieving server information\n" if $self->verbose;
255              
256 0 0       0 $self->report->server( $self->irc->server_name )
257             unless $self->report->server;
258              
259 0         0 my $irc = $self->irc;
260            
261 0         0 my $report = $self->report;
262            
263 0   0     0 my $network = $irc->isupport('NETWORK') || $irc->server_name;
264 0         0 $report->netname($network);
265              
266 0   0     0 $report->ircd( $irc->server_version // 'Not Available' );
267             ## yield off commands to grab anything else needed:
268             ## - LUSERS (maybe, unless we have counts already)
269             ## - LINKS
270             ## - LIST
271             ## stagger them out at reasonable intervals to avoid flood prot:
272 0         0 my $alrm = 2;
273 0         0 for my $cmd (qw/list links lusers/) {
274 0         0 $kernel->alarm_add('b_issue_cmd', time + $alrm, $cmd);
275 0         0 $alrm += $self->{interval};
276             }
277             }
278              
279             sub b_issue_cmd {
280 0     0 0 0 my ($self, $cmd) = @_[OBJECT, ARG0];
281            
282 0 0       0 $self->report->server( $self->irc->server_name )
283             unless $self->report->server;
284              
285             ## most servers will announce lusers at connect-time:
286 0 0 0     0 return if $cmd eq 'lusers' and $self->{State}->{Lusers};
287            
288 0 0       0 warn "-> Issuing: $cmd\n" if $self->verbose;
289 0         0 $self->irc->yield($cmd);
290             }
291              
292             sub b_check_timeout {
293 0     0 0 0 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
294 0         0 my $irc = $self->irc;
295 0         0 my $report = $self->report;
296            
297 0         0 my $shutdown = 0;
298            
299 0         0 my @states = qw/Links Lusers MOTD List/;
300 0         0 my $stc = 0;
301 0         0 for my $state (@states) {
302 0 0       0 next unless $self->{State}->{$state};
303 0         0 $stc++;
304 0 0       0 warn "-> have state: $state\n" if $self->verbose;
305             }
306            
307 0 0       0 $shutdown = 1 if $stc == scalar @states;
308              
309 0   0     0 my $startedat = $report->startedat || 0;
310 0 0       0 if (time - $startedat > $self->{timeout}) {
311 0         0 $self->failed("Timed out");
312 0         0 ++$shutdown;
313             }
314              
315 0 0       0 if ($shutdown) {
316 0 0       0 warn "-> Posting shutdown to own session\n" if $self->verbose;
317 0 0       0 $kernel->post( $_[SESSION], 'shutdown' )
318             if $_[SESSION] eq $_[SENDER];
319             }
320            
321 0         0 $kernel->alarm( 'b_check_timeout', time + 1 );
322             }
323              
324             ## PoCo::IRC handlers
325              
326             sub irc_connected {
327 0     0 0 0 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
328             ## report connected status; irc_001 handles the rest
329 0         0 my $report = $self->report;
330 0         0 $report->status('INIT');
331 0         0 $report->connectedat(time());
332             }
333              
334             sub irc_disconnected {
335 0     0 0 0 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
336             ## we're done, clean up and report such
337 0 0       0 $self->failed("irc_disconnected") unless $self->done;
338 0 0       0 $self->report->server($_[ARG0]) unless $self->report->server;
339 0         0 $self->done(1);
340             }
341              
342             sub irc_socketerr {
343 1     1 0 34562 my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
344 1         2 my $err = $_[ARG0];
345 1         78 $self->failed("irc_socketerr: $err");
346 1         6 $kernel->call( $_[SESSION], 'shutdown' );
347             }
348              
349             sub irc_error {
350 0     0 0   my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
351 0           my $err = $_[ARG0];
352             ## errored out. clean up and report failure status
353 0 0         $self->failed("irc_error: $err") unless $self->done;
354 0           $kernel->call( $_[SESSION], 'shutdown' );
355             }
356              
357             sub irc_001 {
358 0     0 0   my ($self, $kernel, $heap) = @_[OBJECT, KERNEL, HEAP];
359 0           $self->report->status('CONNECTED');
360 0           my $this_server = $self->irc->server_name;
361 0 0         $self->report->server($this_server) if $this_server;
362             ## let things settle out, then b_retrieve_info:
363 0           $kernel->alarm('b_retrieve_info', time + 3);
364             }
365              
366             sub irc_375 {
367             ## Start of MOTD
368 0     0 0   my ($self, $server) = @_[OBJECT, ARG0];
369 0           my $report = $self->report;
370 0           $report->blank_motd;
371 0           $report->motd( "MOTD for $server:" );
372             }
373              
374             sub irc_372 {
375             ## MOTD line
376 0     0 0   my ($self) = $_[OBJECT];
377 0           my $report = $self->report;
378 0           $report->motd( $_[ARG1] );
379             }
380              
381             sub irc_376 {
382             ## End of MOTD
383 0     0 0   my ($self) = $_[OBJECT];
384 0           my $report = $self->report;
385 0           $report->motd( "End of MOTD." );
386 0           $self->{State}->{MOTD} = 1;
387             }
388              
389             sub irc_364 {
390             ## LINKS, if we can get it
391             ## FIXME -- also grab ARG2 and try to create useful hash?
392 0     0 0   my ($self) = $_[OBJECT];
393 0           my $rawline;
394 0 0         return unless $rawline = $_[ARG1];
395 0           push(@{ $self->{ListLinks} }, $_[ARG1]);
  0            
396             }
397              
398             sub irc_365 {
399             ## end of LINKS
400 0     0 0   my $self = $_[OBJECT];
401 0           $self->report->links( $self->{ListLinks} );
402 0           $self->{State}->{Links} = 1;
403             }
404              
405             sub irc_251 {
406 0     0 0   my ($self) = $_[OBJECT];
407 0           my $report = $self->report;
408 0           $self->{State}->{Lusers} = 1;
409            
410 0           my $rawline;
411             ## LUSERS
412             ## may require some fuckery ...
413             ## may vary by IRCD, but in theory it should be something like:
414             ## 'There are X users and Y invisible on Z servers'
415 0 0         return unless $rawline = $_[ARG2]->[0];
416 0           my @chunks = split ' ', $rawline;
417 0           my($users, $i);
418 0           while (my $chunk = shift @chunks) {
419 0 0         if ($chunk =~ /^[0-9]+$/) {
420 0           $users += $chunk;
421 0 0         last if ++$i == 2;
422             }
423             }
424 0   0       $report->users($users||0)
425             }
426              
427             sub irc_252 {
428             ## LUSERS oper count
429 0     0 0   my ($self) = $_[OBJECT];
430 0           my $report = $self->report;
431 0           my $rawline = $_[ARG1];
432 0           my ($count) = $rawline =~ /^([0-9]+)/;
433 0   0       $report->opers($count||0);
434             }
435              
436             sub irc_322 {
437             ## LIST
438 0     0 0   my ($self) = $_[OBJECT];
439 0           my $report = $self->report;
440 0   0       my $split = $_[ARG2] // return;
441 0           my ($chan, $users, $topic) = @$split;
442            
443 0           $chan = decode_irc($chan);
444 0           $topic = decode_irc( strip_color(strip_formatting($topic)) );
445            
446 0   0       $users //= 0;
447 0   0       $topic //= '';
448            
449             ## Add a hash element
450 0           $report->add_channel($chan, $users, $topic);
451             }
452              
453             sub irc_323 {
454             ## LIST ended
455 0     0 0   my ($self) = $_[OBJECT];
456 0           $self->{State}->{List} = 1;
457             }
458              
459             1;
460             __END__