File Coverage

blib/lib/Bot/Pastebot/Client/Irc.pm
Criterion Covered Total %
statement 21 260 8.0
branch 0 84 0.0
condition 0 24 0.0
subroutine 7 40 17.5
pod 0 4 0.0
total 28 412 6.8


line stmt bran cond sub pod time code
1             # Rocco's IRC bot stuff.
2              
3             package Bot::Pastebot::Client::Irc;
4              
5 1     1   1185 use strict;
  1         3  
  1         32  
6              
7 1     1   6 use POE::Session;
  1         1  
  1         11  
8 1     1   1246 use POE::Component::IRC::State;
  1         177065  
  1         88  
9              
10             sub MSG_SPOKEN () { 0x01 }
11             sub MSG_WHISPERED () { 0x02 }
12             sub MSG_EMOTED () { 0x04 }
13              
14 1     1   10 use Bot::Pastebot::Conf qw( get_names_by_type get_items_by_name );
  1         2  
  1         90  
15 1         108 use Bot::Pastebot::Data qw(
16             clear_channels fetch_paste_channel delete_paste
17             clear_channel_ignores set_ignore clear_ignore get_ignores
18             add_channel remove_channel channels
19 1     1   5 );
  1         2  
20 1     1   689 use Bot::Pastebot::Server::Http;
  1         5  
  1         165  
21              
22             my %helptext =
23             (
24             help => <<EOS,
25             Commands: help, ignore, ignores, delete, about, uptime. Use help
26             <command> for help on that command Other topics: about wildcards
27             pasteids
28             EOS
29             ignore => <<EOS,
30             Usage: ignore <wildcard> [<channels>] where <wildcard> is a wildcard
31             IP address. It is only ignored for the given channels of those you
32             are an operator on. Put - in front of a mask to remove it. "ignore -"
33             to delete all ignores.
34             EOS
35             ignores => <<EOS,
36             Usage: ignores <channel>. Returns a list of all ignores on <channel>.
37             EOS
38             delete => <<EOS,
39             Usage: delete <pasteid> where <pasteid> has been pasted to the
40             bot. You can only delete pastes to a channel you are an operator on.
41             EOS
42             about => <<EOS,
43             pastebot is intended to reduce the incidence of pasting of large
44             amounts of text to channels, and the aggravation caused those pastes.
45             The user pastes to a web based form (see the /whois for this bot), and
46             this bot announces the URL in the specified channel
47             EOS
48             wildcards => <<EOS,
49             A set of 4 sets of digits or *. Valid masks: 168.76.*.*, 194.237.235.226
50             Invalid masks: 168.76.*, *.76.235.226
51             EOS
52             pasteids => <<EOS,
53             The digits in the paste URL after the host and port. eg. in
54             http://nopaste.snit.ch:8000/22 the pasteid is 22
55             EOS
56             uptime => <<EOS,
57             Display how long the program has been running and how much CPU it has
58             consumed.
59             EOS
60             );
61              
62             # easy to enter, make it suitable to send
63             for my $key (keys %helptext) {
64             $helptext{$key} =~ tr/\n / /s;
65             $helptext{$key} =~ s/\s+$//;
66             }
67              
68             # Return this module's configuration.
69              
70 1     1   7 use Bot::Pastebot::Conf qw(SCALAR LIST REQUIRED);
  1         2  
  1         4117  
71              
72             my %conf = (
73             irc => {
74             name => SCALAR | REQUIRED,
75             server => LIST | REQUIRED,
76             nick => LIST | REQUIRED,
77             uname => SCALAR | REQUIRED,
78             iname => SCALAR | REQUIRED,
79             away => SCALAR | REQUIRED,
80             flags => SCALAR,
81             join_cfg_only => SCALAR,
82             channel => LIST | REQUIRED,
83             quit => SCALAR | REQUIRED,
84             cuinfo => SCALAR | REQUIRED,
85             cver => SCALAR | REQUIRED,
86             ccinfo => SCALAR | REQUIRED,
87             localaddr => SCALAR,
88             nickserv_pass => SCALAR,
89             },
90             );
91              
92 0     0 0   sub get_conf { return %conf }
93              
94             #------------------------------------------------------------------------------
95              
96             sub initialize {
97              
98             # Build a map from IRC name to web server name I could add an extra
99             # key to the irc sections but that would be redundant
100              
101 0     0 0   my %irc_to_web;
102 0           foreach my $webserver (get_names_by_type('web_server')) {
103 0           my %conf = get_items_by_name($webserver);
104 0           $irc_to_web{$conf{irc}} = $webserver;
105             }
106              
107 0           foreach my $server (get_names_by_type('irc')) {
108 0           my %conf = get_items_by_name($server);
109              
110 0           my $web_alias = $irc_to_web{$server};
111 0           my $irc = POE::Component::IRC::State->spawn();
112              
113             POE::Session->create(
114             inline_states => {
115             _start => sub {
116 0     0     my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
117              
118 0           $kernel->alias_set( "irc_client_$server" );
119 0           $irc->yield( register => 'all' );
120              
121 0           $heap->{server_index} = 0;
122              
123             # Keep-alive timer.
124 0           $kernel->delay( autoping => 300 );
125              
126 0           $kernel->yield( 'connect' );
127             },
128              
129             autoping => sub {
130 0     0     my ($kernel, $heap) = @_[KERNEL, HEAP];
131 0 0         $irc->yield( userhost => $heap->{my_nick})
132             unless $heap->{seen_traffic};
133 0           $heap->{seen_traffic} = 0;
134 0           $kernel->delay( autoping => 300 );
135             },
136              
137             connect => sub {
138 0     0     my ($kernel, $heap) = @_[KERNEL, HEAP];
139              
140 0           my $chosen_server = $conf{server}->[$heap->{server_index}];
141 0           my $chosen_port = 6667;
142 0 0         if ($chosen_server =~ s/[\s\:]+(\S+)\s*$//) {
143 0           $chosen_port = $1;
144             }
145              
146             # warn "server($chosen_server) port($chosen_port)";
147              
148 0           $heap->{nick_index} = 0;
149 0           $heap->{my_nick} = $conf{nick}->[$heap->{nick_index}];
150              
151 0           $irc->yield(
152             connect => {
153             Debug => 1,
154             Nick => $heap->{my_nick},
155             Server => $chosen_server,
156             Port => $chosen_port,
157             Username => $conf{uname},
158             Ircname => $conf{iname},
159             LocalAddr => $conf{localaddr},
160             }
161             );
162              
163 0           $heap->{server_index}++;
164 0 0         $heap->{server_index} = 0 if $heap->{server_index} >= @{$conf{server}};
  0            
165             },
166              
167             join => sub {
168 0     0     my ($kernel, $channel) = @_[KERNEL, ARG0];
169 0           $irc->yield( join => $channel );
170             },
171              
172             irc_msg => sub {
173 0     0     my ($kernel, $heap, $sender, $msg) = @_[KERNEL, HEAP, ARG0, ARG2];
174              
175 0           my ($nick) = $sender =~ /^([^!]+)/;
176 0           print "Message $msg from $nick\n";
177              
178 0           $msg = remove_colors($msg);
179              
180 0 0         if ($msg =~ /^\s*help(?:\s+(\w+))?\s*$/) {
    0          
    0          
    0          
    0          
181 0   0       my $what = $1 || 'help';
182 0 0         if ($helptext{$what}) {
183 0           $irc->yield( privmsg => $nick, $helptext{$what} );
184             }
185             }
186             elsif ($msg =~ /^\s*ignore\s/) {
187 0 0         unless ($msg =~ /^\s*ignore\s+(\S+)(?:\s+(\S+))?\s*$/) {
188 0           $irc->yield(
189             privmsg => $nick, "Usage: ignore <wildcard> [<channels>]"
190             );
191 0           return;
192             }
193 0           my ($mask, $channels) = ($1, $2);
194 0 0 0       unless (
195             $mask =~ /^-?\d+(\.(\*|\d+)){3}$/ || $mask eq '-'
196             ) {
197 0           $irc->yield(
198             privmsg => $nick, "Invalid wildcard. Try: help wildcards"
199             );
200 0           return;
201             }
202 0           my @igchans;
203 0 0         if ($channels) {
204 0           @igchans = split ',', lc $channels;
205             }
206             else {
207 0           @igchans = map lc, channels($conf{name});
208             }
209             # only the channels the user is an operator on
210 0 0         @igchans = grep {
211 0           exists $heap->{users}{$_}{$nick}{mode} and
212             $heap->{users}{$_}{$nick}{mode} =~ /@/
213             } @igchans;
214 0 0         @igchans or return;
215              
216 0 0         if ($mask eq '-') {
    0          
217 0           for my $chan (@igchans) {
218 0           clear_channel_ignores($conf{name}, $chan);
219 0           print "Nick '$nick' deleted all ignores on $chan\n";
220             }
221             $irc->yield(
222 0           privmsg => $nick => "Removed all ignores on @igchans"
223             );
224             }
225             elsif ($mask =~ /^-(.*)$/) {
226 0           my $clearmask = $1;
227 0           for my $chan (@igchans) {
228 0           clear_ignore($conf{name}, $chan, $clearmask);
229             }
230             $irc->yield(
231 0           privmsg => $nick => "Removed ignore $clearmask on @igchans"
232             );
233             }
234             else {
235 0           for my $chan (@igchans) {
236 0           set_ignore($conf{name}, $chan, $mask);
237             }
238             $irc->yield(
239 0           privmsg => $nick => "Added ignore mask $mask on @igchans"
240             );
241             }
242             }
243             elsif ($msg =~ /^\s*ignores\s/) {
244 0 0         unless ($msg =~ /^\s*ignores\s+(\#\S+)\s*$/) {
245 0           $irc->yield( privmsg => $nick, "Usage: ignores <channel>" );
246 0           return;
247             }
248 0           my $channel = lc $1;
249 0           my @masks = get_ignores($conf{name}, $channel);
250 0 0         unless (@masks) {
251 0           $irc->yield( privmsg => $nick, "No ignores on $channel" );
252 0           return;
253             }
254 0           my $text = join " ", @masks;
255 0 0         substr($text, 100) = '...' unless length $text < 100;
256 0           $irc->yield( privmsg => $nick, "Ignores on $channel are: $text" );
257             }
258             elsif ($msg =~ /^\s*delete\s/) {
259 0 0         unless ($msg =~ /^\s*delete\s+(\d+)\s*$/) {
260 0           $irc->yield( privmsg => $nick, "Usage: delete <pasteid>" );
261 0           return;
262             }
263 0           my $pasteid = $1;
264 0           my $paste_chan = fetch_paste_channel($pasteid);
265              
266 0 0         if (defined $paste_chan) {
267 0 0         if ($heap->{users}{$paste_chan}{$nick}{mode} =~ /@/) {
268 0 0         delete_paste($conf{name}, $paste_chan, $pasteid, $nick)
269             or print "It didn't delete!\n";
270 0           $irc->yield( privmsg => $nick => "Deleted paste $pasteid" );
271             }
272             else {
273 0           $irc->yield(
274             privmsg => $nick =>
275             "Paste $pasteid was sent to $paste_chan - " .
276             "you aren't a channel operator on $paste_chan"
277             );
278             }
279             }
280             else {
281 0           $irc->yield( privmsg => $nick => "No such paste" );
282             }
283             }
284             elsif ($msg =~ /^\s*uptime\s*$/) {
285 0           my ($user_time, $system_time) = (times())[0,1];
286 0   0       my $wall_time = (time() - $^T) || 1;
287 0           my $load_average = sprintf(
288             "%.4f", ($user_time+$system_time) / $wall_time
289             );
290 0           $irc->yield(
291             privmsg => $nick,
292             "I was started on " . scalar(gmtime($^T)) . " GMT. " .
293             "I've been active for " . format_elapsed($wall_time, 2) . ". " .
294             sprintf(
295             "I have used about %.2f%% of a CPU during my lifespan.",
296             (($user_time+$system_time)/$wall_time) * 100
297             )
298             );
299             }
300             },
301              
302             # negative on /whois
303             irc_401 => sub {
304 0     0     my ($kernel, $heap, $msg) = @_[KERNEL, HEAP, ARG1];
305              
306 0           my ($nick) = split ' ', $msg;
307 0           delete $heap->{work}{lc $nick};
308             },
309              
310             # Nick is in use
311             irc_433 => sub {
312 0     0     my ($kernel, $heap) = @_[KERNEL, HEAP];
313              
314 0           $heap->{nick_index}++;
315 0           my $newnick = $conf{nick}->[$heap->{nick_index} % @{$conf{nick}}];
  0            
316 0 0         if ($heap->{nick_index} >= @{$conf{nick}}) {
  0            
317 0           $newnick .= $heap->{nick_index} - @{$conf{nick}};
  0            
318 0           $kernel->delay( ison => 120 );
319             }
320 0           $heap->{my_nick} = $newnick;
321              
322 0           warn "Nickclash, now trying $newnick\n";
323 0           $irc->yield( nick => $newnick );
324             },
325              
326             ison => sub {
327 0     0     $irc->yield( ison => @{$conf{nick}} );
  0            
328             },
329              
330             # ISON reply
331             irc_303 => sub {
332 0     0     my ($kernel, $heap, $nicklist) = @_[KERNEL, HEAP, ARG1];
333              
334 0           my @nicklist = split " ", lc $nicklist;
335 0           for my $totry (@{$conf{nick}}) {
  0            
336 0 0         unless (grep $_ eq lc $totry, @nicklist) {
337 0           $irc->yield( nick => $totry );
338 0           return;
339             }
340             }
341 0           $kernel->delay( ison => 120 );
342             },
343              
344             _stop => sub {
345 0     0     my $kernel = $_[KERNEL];
346 0           $irc->yield( quit => $conf{quit} );
347             },
348              
349             _default => sub {
350 0     0     my ($state, $event, $args, $heap) = @_[STATE, ARG0, ARG1, HEAP];
351 0   0       $args ||= [ ];
352 0           print "default $state = $event (@$args)\n";
353 0           $heap->{seen_traffic} = 1;
354 0           return 0;
355             },
356              
357             irc_001 => sub {
358 0     0     my ($kernel, $heap) = @_[KERNEL, HEAP];
359              
360 0 0         if (defined $conf{flags}) {
361 0           $irc->yield( mode => $heap->{my_nick} => $conf{flags} );
362             }
363 0           $irc->yield( away => $conf{away} );
364              
365 0           foreach my $channel (@{$conf{channel}}) {
  0            
366 0           $channel =~ s/^#//;
367 0           $kernel->yield( join => "\#$channel" );
368             }
369              
370 0 0         if (defined $conf{nickserv_pass}) {
371 0           $irc->yield(
372             privmsg => 'NickServ',
373             "IDENTIFY $conf{nickserv_pass}"
374             );
375             }
376              
377 0           $heap->{server_index} = 0;
378             },
379              
380             announce => sub {
381 0     0     my ($kernel, $heap, $channel, $message) =
382             @_[KERNEL, HEAP, ARG0, ARG1];
383              
384 0           my ($nick, $addr) = $message =~ /^"?(.*?)"? at ([\d\.]+) /;
385              
386 0 0         if (my $data = $irc->nick_info ($nick)) {
387             #TODO: maybe check $addr with $data->{Host} ?
388             # instead of the simple nick test below
389             }
390              
391 0 0 0       if ( $nick eq "Someone"
392             or $irc->is_channel_member( $channel, $nick)) {
393 0           $irc->yield( privmsg => $channel => $message );
394             }
395             },
396              
397             irc_ctcp_version => sub {
398 0     0     my ($kernel, $sender) = @_[KERNEL, ARG0];
399 0           my $who = (split /!/, $sender)[0];
400 0           print "ctcp version from $who\n";
401 0           $irc->yield( ctcpreply => $who, "VERSION $conf{cver}" );
402             },
403              
404             irc_ctcp_clientinfo => sub {
405 0     0     my ($kernel, $sender) = @_[KERNEL, ARG0];
406 0           my $who = (split /!/, $sender)[0];
407 0           print "ctcp clientinfo from $who\n";
408 0           $irc->yield( ctcpreply => $who, "CLIENTINFO $conf{ccinfo}" );
409             },
410              
411             irc_ctcp_userinfo => sub {
412 0     0     my ($kernel, $sender) = @_[KERNEL, ARG0];
413 0           my $who = (split /!/, $sender)[0];
414 0           print "ctcp userinfo from $who\n";
415 0           $irc->yield( ctcpreply => $who, "USERINFO $conf{cuinfo}" );
416             },
417              
418             irc_invite => sub {
419 0     0     my ($kernel, $who, $where) = @_[KERNEL, ARG0, ARG1];
420 0           $where =~ s/^#//;
421 0 0 0       if ( $conf{join_cfg_only} &&
  0            
422             1 > grep $_ eq $where, @{$conf{channel}} ) {
423 0           print "$who invited me to $where, but i'm not allowed\n";
424             }
425             else {
426 0           $kernel->yield( join => "#$where" )
427             }
428             },
429              
430             irc_join => sub {
431 0     0     my ($kernel, $heap, $who, $where) = @_[KERNEL, HEAP, ARG0, ARG1];
432 0           my ($nick) = $who =~ /^([^!]+)/;
433 0 0         if (lc ($nick) eq lc($heap->{my_nick})) {
434 0           add_channel($conf{name}, $where);
435 0           $irc->yield( who => $where );
436             }
437 0           @{$heap->{users}{$where}{$nick}}{qw(ident host)} =
  0            
438             (split /[!@]/, $who, 8)[1, 2];
439             },
440              
441             irc_kick => sub {
442 0     0     my ($kernel, $heap, $who, $where, $nick, $reason)
443             = @_[KERNEL, HEAP, ARG0..ARG3];
444 0           print "$nick was kicked from $where by $who: $reason\n";
445 0           delete $heap->{users}{$where}{$nick};
446 0 0         if (lc($nick) eq lc($heap->{my_nick})) {
447 0           remove_channel($conf{name}, $where);
448 0           delete $heap->{users}{$where};
449             }
450             # $kernel->delay( join => 15 => $where );
451             },
452              
453             irc_quit => sub {
454 0     0     my ($kernel, $heap, $who, $what) = @_[KERNEL, HEAP, ARG0, ARG1];
455              
456 0           my ($nick) = $who =~ /^([^!]+)/;
457 0           for (keys %{$heap->{users}}) {
  0            
458 0           delete $heap->{users}{$_}{$nick};
459             }
460             },
461              
462             irc_part => sub {
463 0     0     my ($kernel, $heap, $who, $where) = @_[KERNEL, HEAP, ARG0, ARG1];
464              
465 0           my ($nick) = $who =~ /^([^!]+)/;
466 0           delete $heap->{users}{$where}{$nick};
467             },
468              
469             # who reply
470             irc_352 => sub {
471 0     0     my ($kernel, $heap, $what) = @_[KERNEL, HEAP, ARG1];
472              
473 0           my @reply = split " ", $what, 8;
474 0           @{$heap->{users}{$reply[0]}{$reply[4]}}{qw(ident host mode real)} = (
  0            
475             $reply[1], $reply[2], $reply[5], $reply[7]
476             );
477             },
478              
479             irc_mode => sub {
480 0     0     my ($kernel, $heap, $issuer, $location, $modestr, @targets)
481             = @_[KERNEL, HEAP, ARG0..$#_];
482              
483 0           my $set = "+";
484 0           for (split //, $modestr) {
485 0 0 0       $set = $_ if ($_ eq "-" or $_ eq "+");
486 0 0         if (/[bklovehI]/) { # mode has argument
487 0           my $target = shift @targets;
488 0 0         if ($_ eq "o") {
489 0 0         if ($set eq "+") {
490 0 0         $heap->{users}{$location}{$target}{mode} .= '@'
491             unless $heap->{users}{$location}{$target}{mode} =~ /\@/;
492             }
493             else {
494 0           $heap->{users}{$location}{$target}{mode} =~ s/\@//;
495             }
496             }
497             }
498             }
499             },
500              
501             # end of /names
502 0     0     irc_315 => sub {},
503             # end of /who
504 0     0     irc_366 => sub {},
505              
506             irc_disconnected => sub {
507 0     0     my ($kernel, $heap, $server) = @_[KERNEL, HEAP, ARG0];
508 0           print "Lost connection to server $server.\n";
509 0           clear_channels($conf{name});
510 0           delete $heap->{users};
511 0           $kernel->delay( connect => 60 );
512             },
513              
514             irc_error => sub {
515 0     0     my ($kernel, $heap, $error) = @_[KERNEL, HEAP, ARG0];
516 0           print "Server error occurred: $error\n";
517 0           clear_channels($conf{name});
518 0           delete $heap->{users};
519 0           $kernel->delay( connect => 60 );
520             },
521              
522             irc_socketerr => sub {
523 0     0     my ($kernel, $heap, $error) = @_[KERNEL, HEAP, ARG0];
524 0           print "IRC client ($server): socket error occurred: $error\n";
525 0           clear_channels($conf{name});
526 0           delete $heap->{users};
527 0           $kernel->delay( connect => 60 );
528             },
529              
530             irc_public => sub {
531 0     0     my ($kernel, $heap, $who, $where, $msg) = @_[KERNEL, HEAP, ARG0..ARG2];
532 0           $who = (split /!/, $who)[0];
533 0           $where = $where->[0];
534 0           print "<$who:$where> $msg\n";
535              
536 0           $heap->{seen_traffic} = 1;
537              
538             # Do something with input here?
539             # If so, remove colors from it first.
540             },
541             },
542 0           );
543             }
544             }
545              
546             # Helper function. Display a number of seconds as a formatted period
547             # of time. NOT A POE EVENT HANDLER.
548              
549             sub format_elapsed {
550 0     0 0   my ($secs, $precision) = @_;
551 0           my @fields;
552              
553             # If the elapsed time can be measured in weeks.
554 0 0         if (my $part = int($secs / 604800)) {
555 0           $secs %= 604800;
556 0           push(@fields, $part . 'w');
557             }
558              
559             # If the remaining time can be measured in days.
560 0 0         if (my $part = int($secs / 86400)) {
561 0           $secs %= 86400;
562 0           push(@fields, $part . 'd');
563             }
564              
565             # If the remaining time can be measured in hours.
566 0 0         if (my $part = int($secs / 3600)) {
567 0           $secs %= 3600;
568 0           push(@fields, $part . 'h');
569             }
570              
571             # If the remaining time can be measured in minutes.
572 0 0         if (my $part = int($secs / 60)) {
573 0           $secs %= 60;
574 0           push(@fields, $part . 'm');
575             }
576              
577             # If there are any seconds remaining, or the time is nothing.
578 0 0 0       if ($secs || !@fields) {
579 0           push(@fields, $secs . 's');
580             }
581              
582             # Reduce precision, if requested.
583 0   0       pop(@fields) while $precision and @fields > $precision;
584              
585             # Combine the parts.
586 0           join(' ', @fields);
587             }
588              
589             # Helper functions. Remove color codes from a message.
590              
591             sub remove_colors {
592 0     0 0   my $msg = shift;
593              
594             # Indigoid supplied these regexps to extract colors.
595 0           $msg =~ s/[\x02\x0F\x11\x12\x16\x1d\x1f]//g; # Regular attributes.
596 0           $msg =~ s/\x03[0-9,]*//g; # mIRC colors.
597 0           $msg =~ s/\x04[0-9a-f]+//ig; # Other colors.
598              
599 0           return $msg;
600             }
601              
602             1;