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