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.93';
4 80     80   61582 use strict;
  80         418  
  80         2831  
5 80     80   453 use warnings FATAL => 'all';
  80         182  
  80         3235  
6 80     80   476 use Carp;
  80         184  
  80         5168  
7 80     80   558 use POE::Filter::IRCD;
  80         182  
  80         2652  
8 80     80   560 use File::Basename qw(fileparse);
  80         441  
  80         9263  
9 80     80   597 use base qw(POE::Filter);
  80         211  
  80         289396  
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 793 my ($package, %self) = @_;
140              
141 124         889 $self{lc $_} = delete $self{$_} for keys %self;
142 124         432 $self{BUFFER} = [ ];
143 124         465 $self{_ircd} = POE::Filter::IRCD->new();
144 124 50       2733 $self{chantypes} = [ '#', '&' ] if ref $self{chantypes} ne 'ARRAY';
145              
146 124         773 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 548 my ($self, $ref) = @_;
169 180 50 33     925 return if ref $ref ne 'ARRAY' || !@{ $ref };
  180         874  
170 180         570 $self->{chantypes} = $ref;
171 180         442 return 1;
172             }
173              
174             sub identifymsg {
175 90     90 1 257 my ($self, $switch) = @_;
176 90         246 $self->{identifymsg} = $switch;
177 90         240 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 3288     3288 1 46050 my ($self) = @_;
189 3288 100       5108 my $line = shift @{ $self->{BUFFER} } or return [ ];
  3288         10356  
190              
191 2580 50 33     17816 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 2580 100 100     7413 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 2546         9334 };
204              
205 2546         9560 for my $cmd (keys %irc_cmds) {
206 8407 100       104011 if ($event->{name} =~ $cmd) {
207 2098         7961 $irc_cmds{$cmd}->($self, $event, $line);
208 2098         11207 return [ $event ];
209             }
210             }
211              
212             # default
213 448 100 50     1870 unshift( @{ $line->{params} }, _decolon( $line->{prefix} || '' ) ) if $line->{prefix};
  357         1749  
214 448         1133 $event->{args} = $line->{params};
215 448         2058 return [ $event ];
216             }
217              
218             sub get_one_start {
219 2580     2580 1 8800542 my ($self, $lines) = @_;
220 2580         4472 push @{ $self->{BUFFER} }, @$lines;
  2580         6176  
221 2580         5660 return;
222             }
223              
224             sub put {
225 27     27 1 101 my ($self, $lineref) = @_;
226 27         73 my $quoted = [ ];
227 27         112 push @$quoted, _ctcp_quote($_) for @$lineref;
228 27         96 return $quoted;
229             }
230              
231             # Properly CTCP-quotes a message. Whoop.
232             sub _ctcp_quote {
233 27     27   70 my ($line) = @_;
234              
235 27         94 $line = _low_quote( $line );
236             #$line =~ s/\\/\\\\/g;
237 27         81 $line =~ s/\001/\\a/g;
238              
239 27         114 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   80 my ($msg) = @_;
247 34         79 my (@chunks, $ctcp, $text);
248              
249             # CHUNG! CHUNG! CHUNG!
250              
251 34 50       106 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         156 $msg = _low_dequote( $msg );
257              
258             # Filter misplaced \001s before processing... (Thanks, tchrist!)
259 34 100       159 substr($msg, rindex($msg, "\001"), 1, '\\a')
260             if ($msg =~ tr/\001//) % 2 != 0;
261              
262 34 100       130 return if $msg !~ tr/\001//;
263              
264 33         132 @chunks = split /\001/, $msg;
265 33 50       137 shift @chunks if !length $chunks[0]; # FIXME: Is this safe?
266              
267 33         98 for (@chunks) {
268             # Dequote unnecessarily quoted chars, and convert escaped \'s and ^A's.
269 35         108 s/\\([^\\a])/$1/g;
270 35         76 s/\\\\/\\/g;
271 35         83 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       191 if ($msg =~ /^\001/) {
278 33         103 push @$ctcp, shift @chunks;
279             }
280              
281 33         110 while (@chunks) {
282 1         2 push @$text, shift @chunks;
283 1 50       6 push @$ctcp, shift @chunks if @chunks;
284             }
285              
286 33         125 return ($ctcp, $text);
287             }
288              
289             sub _decolon {
290 2450     2450   4799 my ($line) = @_;
291              
292 2450         5060 $line =~ s/^://;
293 2450         8106 return $line;
294             }
295              
296             ## no critic (Subroutines::ProhibitExcessComplexity)
297             sub _get_ctcp {
298 34     34   93 my ($self, $line) = @_;
299              
300             # Is this a CTCP request or reply?
301 34 100       282 my $ctcp_type = $line->{command} eq 'PRIVMSG' ? 'ctcp' : 'ctcpreply';
302              
303             # CAPAP IDENTIFY-MSG is only applied to ACTIONs
304 34         148 my ($msg, $identified) = ($line->{params}->[1], undef);
305 34 50 33     180 ($msg, $identified) = _split_idmsg($msg) if $self->{identifymsg} && $msg =~ /^.ACTION/;
306              
307 34         76 my $events = [ ];
308 34         171 my ($ctcp, $text) = _ctcp_dequote($msg);
309              
310 34 100       145 if (!defined $ctcp) {
311 1 50       2 warn "Received malformed CTCP message: $msg\n" if $self->{debug};
312 1         5 return $events;
313             }
314              
315 33 100       222 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         87 my ($name, $args);
321 33         109 CTCP: for my $string ($ctcp->[0]) {
322 33 50       265 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       146 if (lc $name eq 'dcc') {
331 11         36 my ($dcc_type, $rest);
332              
333 11 50       118 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         117 my ($handler) = grep { $dcc_type =~ /$_/ } keys %dcc_types;
  22         903  
344 11 50       62 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         95 my @dcc_args = $dcc_types{$handler}->($nick, $dcc_type, $rest);
350 11 50       64 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         144 };
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       238 };
    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         205 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   84 my ($line) = @_;
402 27         359 my %enquote = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");
403              
404 27 50       97 if (!defined $line) {
405 0         0 croak 'Not enough arguments to POE::Filter::IRC::Compat->_low_quote';
406             }
407              
408 27 50       99 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         103 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   103 my ($line) = @_;
419 34         408 my %dequote = (n => "\012", r => "\015", 0 => "\0", "\cP" => "\cP");
420              
421 34 50       123 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       138 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