File Coverage

blib/lib/POE/Filter/IRC/Compat.pm
Criterion Covered Total %
statement 115 151 76.1
branch 40 84 47.6
condition 8 17 47.0
subroutine 18 21 85.7
pod 8 8 100.0
total 189 281 67.2


line stmt bran cond sub pod time code
1             package POE::Filter::IRC::Compat;
2             $POE::Filter::IRC::Compat::VERSION = '6.95';
3 80     80   148700 use strict;
  80         172  
  80         3489  
4 80     80   432 use warnings FATAL => 'all';
  80         176  
  80         5034  
5 80     80   473 use Carp;
  80         189  
  80         5187  
6 80     80   465 use POE::Filter::IRCD;
  80         156  
  80         2435  
7 80     80   396 use File::Basename qw(fileparse);
  80         157  
  80         7775  
8 80     80   680 use base qw(POE::Filter);
  80         237  
  80         343268  
9              
10             my %irc_cmds = (
11             qr/^\d{3}$/ => sub {
12             my ($self, $event, $line) = @_;
13             $event->{args}->[0] = _decolon( $line->{prefix} );
14             shift @{ $line->{params} };
15             if ( $line->{params}->[0] && $line->{params}->[0] =~ /\x20/ ) {
16             $event->{args}->[1] = $line->{params}->[0];
17             }
18             else {
19             $event->{args}->[1] = join(' ', ( map { /\x20/ ? ":$_" : $_ } @{ $line->{params} } ) );
20             }
21             $event->{args}->[2] = $line->{params};
22             },
23             qr/^cap$/ => sub {
24             my ($self, $event, $line) = @_;
25              
26             for (my $i = 0; ; $i++) {
27             last if !defined $line->{params}[$i+1];
28             $event->{args}[$i] = $line->{params}[$i+1];
29             }
30             },
31             qr/^notice$/ => sub {
32             my ($self, $event, $line) = @_;
33              
34             if (defined $line->{prefix} && $line->{prefix} =~ /!/) {
35             $event->{args} = [
36             _decolon( $line->{prefix} ),
37             [split /,/, $line->{params}->[0]],
38             ($self->{identifymsg}
39             ? _split_idmsg($line->{params}->[1])
40             : $line->{params}->[1]
41             ),
42             ];
43             }
44             else {
45             $event->{name} = 'snotice';
46             $event->{args} = [
47             $line->{params}->[1],
48             $line->{params}->[0],
49             (defined $line->{prefix} ? _decolon($line->{prefix}) : ()),
50             ];
51             }
52             },
53             qr/^privmsg$/ => sub {
54             my ($self, $event, $line) = @_;
55             if ( grep { index( $line->{params}->[0], $_ ) >= 0 } @{ $self->{chantypes} } ) {
56             $event->{args} = [
57             _decolon( $line->{prefix} ),
58             [split /,/, $line->{params}->[0]],
59             ($self->{identifymsg}
60             ? _split_idmsg($line->{params}->[1])
61             : $line->{params}->[1]
62             ),
63             ];
64             $event->{name} = 'public';
65             }
66             else {
67             $event->{args} = [
68             _decolon( $line->{prefix} ),
69             [split /,/, $line->{params}->[0]],
70             ($self->{identifymsg}
71             ? _split_idmsg($line->{params}->[1])
72             : $line->{params}->[1]
73             ),
74             ];
75             $event->{name} = 'msg';
76             }
77             },
78             qr/^invite$/ => sub {
79             my ($self, $event, $line) = @_;
80             shift( @{ $line->{params} } );
81             unshift( @{ $line->{params} }, _decolon( $line->{prefix} || '' ) ) if $line->{prefix};
82             $event->{args} = $line->{params};
83             },
84             );
85              
86             # the magic cookie jar
87             my %dcc_types = (
88             qr/^(?:CHAT|SEND)$/ => sub {
89             my ($nick, $type, $args) = @_;
90             my ($file, $addr, $port, $size);
91             return if !(($file, $addr, $port, $size) = $args =~ /^(".+"|[^ ]+) +(\d+) +(\d+)(?: +(\d+))?/);
92              
93             if ($file =~ s/^"//) {
94             $file =~ s/"$//;
95             $file =~ s/\\"/"/g;
96             }
97             $file = fileparse($file);
98              
99             return (
100             $port,
101             {
102             nick => $nick,
103             type => $type,
104             file => $file,
105             size => $size,
106             addr => $addr,
107             port => $port,
108             },
109             $file,
110             $size,
111             $addr,
112             );
113             },
114             qr/^(?:ACCEPT|RESUME)$/ => sub {
115             my ($nick, $type, $args) = @_;
116             my ($file, $port, $position);
117             return if !(($file, $port, $position) = $args =~ /^(".+"|[^ ]+) +(\d+) +(\d+)/);
118              
119             $file =~ s/^"|"$//g;
120             $file = fileparse($file);
121              
122             return (
123             $port,
124             {
125             nick => $nick,
126             type => $type,
127             file => $file,
128             size => $position,
129             port => $port,
130             },
131             $file,
132             $position,
133             );
134             },
135             );
136              
137             sub new {
138 124     124 1 729 my ($package, %self) = @_;
139              
140 124         755 $self{lc $_} = delete $self{$_} for keys %self;
141 124         433 $self{BUFFER} = [ ];
142 124         512 $self{_ircd} = POE::Filter::IRCD->new();
143 124 50       2613 $self{chantypes} = [ '#', '&' ] if ref $self{chantypes} ne 'ARRAY';
144              
145 124         706 return bless \%self, $package;
146             }
147              
148             sub clone {
149 0     0 1 0 my $self = shift;
150 0         0 my $nself = { };
151 0         0 $nself->{$_} = $self->{$_} for keys %{ $self };
  0         0  
152 0         0 $nself->{BUFFER} = [ ];
153 0         0 return bless $nself, ref $self;
154             }
155              
156             # Set/clear the 'debug' flag.
157             sub debug {
158 0     0 1 0 my ($self, $flag) = @_;
159 0 0       0 if (defined $flag) {
160 0         0 $self->{debug} = $flag;
161 0         0 $self->{_ircd}->debug($flag);
162             }
163 0         0 return $self->{debug};
164             }
165              
166             sub chantypes {
167 181     181 1 508 my ($self, $ref) = @_;
168 181 50 33     962 return if ref $ref ne 'ARRAY' || !@{ $ref };
  181         793  
169 181         615 $self->{chantypes} = $ref;
170 181         426 return 1;
171             }
172              
173             sub identifymsg {
174 91     91 1 232 my ($self, $switch) = @_;
175 91         233 $self->{identifymsg} = $switch;
176 91         220 return;
177             }
178              
179             sub _split_idmsg {
180 0     0   0 my ($line) = @_;
181 0         0 my ($identified, $msg) = split //, $line, 2;
182 0 0       0 $identified = $identified eq '+' ? 1 : 0;
183 0         0 return $msg, $identified;
184             }
185              
186             sub get_one {
187 3303     3303 1 52569 my ($self) = @_;
188 3303 100       5615 my $line = shift @{ $self->{BUFFER} } or return [ ];
  3303         11722  
189              
190 2588 50 33     19328 if (ref $line ne 'HASH' || !$line->{command} || !$line->{params}) {
      33        
191 0 0       0 warn "Received line '$line' that is not IRC protocol\n" if $self->{debug};
192 0         0 return [ ];
193             }
194              
195 2588 100 100     7636 if ($line->{command} =~ /^PRIVMSG|NOTICE$/ && $line->{params}->[1] =~ tr/\001//) {
196 34         152 return $self->_get_ctcp($line);
197             }
198              
199             my $event = {
200             name => lc $line->{command},
201             raw_line => $line->{raw_line},
202 2554         10943 };
203              
204 2554         9584 for my $cmd (keys %irc_cmds) {
205 9104 100       157506 if ($event->{name} =~ $cmd) {
206 2107         8477 $irc_cmds{$cmd}->($self, $event, $line);
207 2107         14096 return [ $event ];
208             }
209             }
210              
211             # default
212 447 100 50     1943 unshift( @{ $line->{params} }, _decolon( $line->{prefix} || '' ) ) if $line->{prefix};
  356         1980  
213 447         1287 $event->{args} = $line->{params};
214 447         2406 return [ $event ];
215             }
216              
217             sub get_one_start {
218 2588     2588 1 9293497 my ($self, $lines) = @_;
219 2588         4843 push @{ $self->{BUFFER} }, @$lines;
  2588         6826  
220 2588         5828 return;
221             }
222              
223             sub put {
224 27     27 1 120 my ($self, $lineref) = @_;
225 27         59 my $quoted = [ ];
226 27         132 push @$quoted, _ctcp_quote($_) for @$lineref;
227 27         133 return $quoted;
228             }
229              
230             # Properly CTCP-quotes a message. Whoop.
231             sub _ctcp_quote {
232 27     27   86 my ($line) = @_;
233              
234 27         97 $line = _low_quote( $line );
235             #$line =~ s/\\/\\\\/g;
236 27         119 $line =~ s/\001/\\a/g;
237              
238 27         139 return "\001$line\001";
239             }
240              
241             # Splits a message into CTCP and text chunks. This is gross. Most of
242             # this is also stolen from Net::IRC, but I (fimm) wrote that too, so it's
243             # used with permission. ;-)
244             sub _ctcp_dequote {
245 34     34   115 my ($msg) = @_;
246 34         70 my (@chunks, $ctcp, $text);
247              
248             # CHUNG! CHUNG! CHUNG!
249              
250 34 50       108 if (!defined $msg) {
251 0         0 croak 'Not enough arguments to POE::Filter::IRC::Compat::_ctcp_dequote';
252             }
253              
254             # Strip out any low-level quoting in the text.
255 34         149 $msg = _low_dequote( $msg );
256              
257             # Filter misplaced \001s before processing... (Thanks, tchrist!)
258 34 100       208 substr($msg, rindex($msg, "\001"), 1, '\\a')
259             if ($msg =~ tr/\001//) % 2 != 0;
260              
261 34 100       174 return if $msg !~ tr/\001//;
262              
263 33         131 @chunks = split /\001/, $msg;
264 33 50       110 shift @chunks if !length $chunks[0]; # FIXME: Is this safe?
265              
266 33         92 for (@chunks) {
267             # Dequote unnecessarily quoted chars, and convert escaped \'s and ^A's.
268 35         98 s/\\([^\\a])/$1/g;
269 35         76 s/\\\\/\\/g;
270 35         118 s/\\a/\001/g;
271             }
272              
273             # If the line begins with a control-A, the first chunk is a CTCP
274             # message. Otherwise, it starts with text and alternates with CTCP
275             # messages. Really stupid protocol.
276 33 50       160 if ($msg =~ /^\001/) {
277 33         112 push @$ctcp, shift @chunks;
278             }
279              
280 33         108 while (@chunks) {
281 1         3 push @$text, shift @chunks;
282 1 50       3 push @$ctcp, shift @chunks if @chunks;
283             }
284              
285 33         151 return ($ctcp, $text);
286             }
287              
288             sub _decolon {
289 2458     2458   5404 my ($line) = @_;
290              
291 2458         5534 $line =~ s/^://;
292 2458         9014 return $line;
293             }
294              
295             ## no critic (Subroutines::ProhibitExcessComplexity)
296             sub _get_ctcp {
297 34     34   91 my ($self, $line) = @_;
298              
299             # Is this a CTCP request or reply?
300 34 100       209 my $ctcp_type = $line->{command} eq 'PRIVMSG' ? 'ctcp' : 'ctcpreply';
301              
302             # CAPAP IDENTIFY-MSG is only applied to ACTIONs
303 34         116 my ($msg, $identified) = ($line->{params}->[1], undef);
304 34 50 33     162 ($msg, $identified) = _split_idmsg($msg) if $self->{identifymsg} && $msg =~ /^.ACTION/;
305              
306 34         74 my $events = [ ];
307 34         123 my ($ctcp, $text) = _ctcp_dequote($msg);
308              
309 34 100       103 if (!defined $ctcp) {
310 1 50       32 warn "Received malformed CTCP message: $msg\n" if $self->{debug};
311 1         11 return $events;
312             }
313              
314 33 100       209 my $nick = defined $line->{prefix} ? (split /!/, $line->{prefix})[0] : undef;
315              
316             # We only process the first CTCP. The only people who send multiple ones
317             # are those who are trying to flood our outgoing queue anyway (e.g. by
318             # having us reply to 20 VERSION requests at a time).
319 33         80 my ($name, $args);
320 33         127 CTCP: for my $string ($ctcp->[0]) {
321 33 50       288 if (!(($name, $args) = $string =~ /^(\w+)(?: +(.*))?/)) {
322             defined $nick
323 0 0       0 ? do { warn "Received malformed CTCP message from $nick: $string\n" if $self->{debug} }
324 0 0       0 : do { warn "Trying to send malformed CTCP message: $string\n" if $self->{debug} }
  0 0       0  
325             ;
326 0         0 last CTCP;
327             }
328              
329 33 100       180 if (lc $name eq 'dcc') {
330 11         23 my ($dcc_type, $rest);
331              
332 11 50       108 if (!(($dcc_type, $rest) = $args =~ /^(\w+) +(.+)/)) {
333             defined $nick
334 0 0       0 ? do { warn "Received malformed DCC request from $nick: $args\n" if $self->{debug} }
335 0 0       0 : do { warn "Trying to send malformed DCC request: $args\n" if $self->{debug} }
  0 0       0  
336             ;
337 0         0 last CTCP;
338              
339             }
340 11         35 $dcc_type = uc $dcc_type;
341              
342 11         66 my ($handler) = grep { $dcc_type =~ /$_/ } keys %dcc_types;
  22         875  
343 11 50       48 if (!$handler) {
344 0 0       0 warn "Unhandled DCC $dcc_type request: $rest\n" if $self->{debug};
345 0         0 last CTCP;
346             }
347              
348 11         54 my @dcc_args = $dcc_types{$handler}->($nick, $dcc_type, $rest);
349 11 50       55 if (!@dcc_args) {
350             defined $nick
351 0 0       0 ? do { warn "Received malformed DCC $dcc_type request from $nick: $rest\n" if $self->{debug} }
352 0 0       0 : do { warn "Trying to send malformed DCC $dcc_type request: $rest\n" if $self->{debug} }
  0 0       0  
353             ;
354 0         0 last CTCP;
355             }
356              
357             push @$events, {
358             name => 'dcc_request',
359             args => [
360             $line->{prefix},
361             $dcc_type,
362             @dcc_args,
363             ],
364             raw_line => $line->{raw_line},
365 11         105 };
366             }
367             else {
368             push @$events, {
369             name => $ctcp_type . '_' . lc $name,
370             args => [
371             $line->{prefix},
372             [split /,/, $line->{params}->[0]],
373             (defined $args ? $args : ''),
374             (defined $identified ? $identified : () ),
375             ],
376             raw_line => $line->{raw_line},
377 22 100       435 };
    50          
378             }
379             }
380              
381             # XXX: I'm not quite sure what this is for, but on FreeNode it adds an
382             # extra bogus event and displays a debug message, so I have disabled it.
383             # FreeNode precedes PRIVMSG and CTCP ACTION messages with '+' or '-'.
384             #if ($text && @$text) {
385             # my $what;
386             # ($what) = $line->{raw_line} =~ /^(:[^ ]+ +\w+ +[^ ]+ +)/
387             # or warn "What the heck? '".$line->{raw_line}."'\n" if $self->{debug};
388             # $text = (defined $what ? $what : '') . ':' . join '', @$text;
389             # $text =~ s/\cP/^P/g;
390             # warn "CTCP: $text\n" if $self->{debug};
391             # push @$events, @{ $self->{_ircd}->get([$text]) };
392             #}
393              
394 33         238 return $events;
395             }
396              
397             # Quotes a string in a low-level, protocol-safe, utterly brain-dead
398             # fashion. Returns the quoted string.
399             sub _low_quote {
400 27     27   62 my ($line) = @_;
401 27         250 my %enquote = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");
402              
403 27 50       89 if (!defined $line) {
404 0         0 croak 'Not enough arguments to POE::Filter::IRC::Compat->_low_quote';
405             }
406              
407 27 50       133 if ($line =~ tr/[\012\015\0\cP]//) { # quote \n, \r, ^P, and \0.
408 0         0 $line =~ s/([\012\015\0\cP])/\cP$enquote{$1}/g;
409             }
410              
411 27         97 return $line;
412             }
413              
414             # Does low-level dequoting on CTCP messages. I hate this protocol.
415             # Yes, I copied this whole section out of Net::IRC.
416             sub _low_dequote {
417 34     34   89 my ($line) = @_;
418 34         336 my %dequote = (n => "\012", r => "\015", 0 => "\0", "\cP" => "\cP");
419              
420 34 50       116 if (!defined $line) {
421 0         0 croak 'Not enough arguments to POE::Filter::IRC::Compat->_low_dequote';
422             }
423              
424             # dequote \n, \r, ^P, and \0.
425             # Thanks to Abigail (abigail@foad.org) for this clever bit.
426 34 50       116 if ($line =~ tr/\cP//) {
427 0         0 $line =~ s/\cP([nr0\cP])/$dequote{$1}/g;
428             }
429              
430 34         126 return $line;
431             }
432              
433             1;
434              
435             =encoding utf8
436              
437             =head1 NAME
438              
439             POE::Filter::IRC::Compat - A filter which converts L
440             output into L events
441              
442             =head1 SYNOPSIS
443              
444             my $filter = POE::Filter::IRC::Compat->new();
445             my @events = @{ $filter->get( [ @lines ] ) };
446             my @msgs = @{ $filter->put( [ @messages ] ) };
447              
448             =head1 DESCRIPTION
449              
450             POE::Filter::IRC::Compat is a L that converts
451             L output into the L
452             compatible event references. Basically a hack, so I could replace
453             L with something that was more
454             generic.
455              
456             Among other things, it converts normal text into thoroughly CTCP-quoted
457             messages, and transmogrifies CTCP-quoted messages into their normal,
458             sane components. Rather what you'd expect a filter to do.
459              
460             A note: the CTCP protocol sucks bollocks. If I ever meet the fellow who
461             came up with it, I'll shave their head and tattoo obscenities on it.
462             Just read the "specification" (F in this distribution)
463             and you'll hopefully see what I mean. Quote this, quote that, quote this
464             again, all in different and weird ways... and who the hell needs to send
465             mixed CTCP and text messages? WTF? It looks like it's practically complexity
466             for complexity's sake -- and don't even get me started on the design of the
467             DCC protocol! Anyhow, enough ranting. Onto the rest of the docs...
468              
469             =head1 METHODS
470              
471             =head2 C
472              
473             Returns a POE::Filter::IRC::Compat object. Takes no arguments.
474              
475             =head2 C
476              
477             Makes a copy of the filter, and clears the copy's buffer.
478              
479             =head2 C
480              
481             Takes an arrayref of L hashrefs and produces an arrayref of
482             L compatible event hashrefs. Yay.
483              
484             =head2 C, C
485              
486             These perform a similar function as C but enable the filter to work with
487             L.
488              
489             =head2 C
490              
491             Takes an array reference of CTCP messages to be properly quoted. This
492             doesn't support CTCPs embedded in normal messages, which is a
493             brain-dead hack in the protocol, so do it yourself if you really need
494             it. Returns an array reference of the quoted lines for sending.
495              
496             =head2 C
497              
498             Takes an optinal true/false value which enables/disables debugging
499             accordingly. Returns the debug status.
500              
501             =head2 C
502              
503             Takes an arrayref of possible channel prefix indicators.
504              
505             =head2 C
506              
507             Takes a boolean to turn on/off the support for CAPAB IDENTIFY-MSG.
508              
509             =head1 AUTHOR
510              
511             Chris 'BinGOs' Williams
512              
513             =head1 SEE ALSO
514              
515             L
516              
517             L
518              
519             L
520              
521             =cut