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.91';
4 80     80   72317 use strict;
  80         375  
  80         2457  
5 80     80   375 use warnings FATAL => 'all';
  80         147  
  80         2764  
6 80     80   402 use Carp;
  80         143  
  80         4402  
7 80     80   483 use POE::Filter::IRCD;
  80         150  
  80         2224  
8 80     80   446 use File::Basename qw(fileparse);
  80         335  
  80         7820  
9 80     80   505 use base qw(POE::Filter);
  80         165  
  80         239914  
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 687 my ($package, %self) = @_;
140              
141 124         731 $self{lc $_} = delete $self{$_} for keys %self;
142 124         383 $self{BUFFER} = [ ];
143 124         425 $self{_ircd} = POE::Filter::IRCD->new();
144 124 50       2231 $self{chantypes} = [ '#', '&' ] if ref $self{chantypes} ne 'ARRAY';
145              
146 124         630 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 461 my ($self, $ref) = @_;
169 180 50 33     847 return if ref $ref ne 'ARRAY' || !@{ $ref };
  180         674  
170 180         503 $self->{chantypes} = $ref;
171 180         464 return 1;
172             }
173              
174             sub identifymsg {
175 91     91 1 204 my ($self, $switch) = @_;
176 91         221 $self->{identifymsg} = $switch;
177 91         177 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 3313     3313 1 37119 my ($self) = @_;
189 3313 100       4229 my $line = shift @{ $self->{BUFFER} } or return [ ];
  3313         8786  
190              
191 2606 50 33     14810 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 2606 100 100     6507 if ($line->{command} =~ /^PRIVMSG|NOTICE$/ && $line->{params}->[1] =~ tr/\001//) {
197 34         148 return $self->_get_ctcp($line);
198             }
199              
200             my $event = {
201             name => lc $line->{command},
202             raw_line => $line->{raw_line},
203 2572         7772 };
204              
205 2572         8118 for my $cmd (keys %irc_cmds) {
206 8199 100       83211 if ($event->{name} =~ $cmd) {
207 2123         6510 $irc_cmds{$cmd}->($self, $event, $line);
208 2123         8964 return [ $event ];
209             }
210             }
211              
212             # default
213 449 100 50     1538 unshift( @{ $line->{params} }, _decolon( $line->{prefix} || '' ) ) if $line->{prefix};
  358         1643  
214 449         977 $event->{args} = $line->{params};
215 449         1644 return [ $event ];
216             }
217              
218             sub get_one_start {
219 2606     2606 1 8518313 my ($self, $lines) = @_;
220 2606         3729 push @{ $self->{BUFFER} }, @$lines;
  2606         5010  
221 2606         4507 return;
222             }
223              
224             sub put {
225 27     27 1 65 my ($self, $lineref) = @_;
226 27         50 my $quoted = [ ];
227 27         102 push @$quoted, _ctcp_quote($_) for @$lineref;
228 27         92 return $quoted;
229             }
230              
231             # Properly CTCP-quotes a message. Whoop.
232             sub _ctcp_quote {
233 27     27   66 my ($line) = @_;
234              
235 27         81 $line = _low_quote( $line );
236             #$line =~ s/\\/\\\\/g;
237 27         76 $line =~ s/\001/\\a/g;
238              
239 27         115 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   82 my ($msg) = @_;
247 34         65 my (@chunks, $ctcp, $text);
248              
249             # CHUNG! CHUNG! CHUNG!
250              
251 34 50       97 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         113 $msg = _low_dequote( $msg );
257              
258             # Filter misplaced \001s before processing... (Thanks, tchrist!)
259 34 100       146 substr($msg, rindex($msg, "\001"), 1, '\\a')
260             if ($msg =~ tr/\001//) % 2 != 0;
261              
262 34 100       109 return if $msg !~ tr/\001//;
263              
264 33         137 @chunks = split /\001/, $msg;
265 33 50       118 shift @chunks if !length $chunks[0]; # FIXME: Is this safe?
266              
267 33         87 for (@chunks) {
268             # Dequote unnecessarily quoted chars, and convert escaped \'s and ^A's.
269 35         91 s/\\([^\\a])/$1/g;
270 35         82 s/\\\\/\\/g;
271 35         100 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       173 if ($msg =~ /^\001/) {
278 33         89 push @$ctcp, shift @chunks;
279             }
280              
281 33         120 while (@chunks) {
282 1         3 push @$text, shift @chunks;
283 1 50       4 push @$ctcp, shift @chunks if @chunks;
284             }
285              
286 33         101 return ($ctcp, $text);
287             }
288              
289             sub _decolon {
290 2476     2476   3926 my ($line) = @_;
291              
292 2476         4287 $line =~ s/^://;
293 2476         6851 return $line;
294             }
295              
296             ## no critic (Subroutines::ProhibitExcessComplexity)
297             sub _get_ctcp {
298 34     34   85 my ($self, $line) = @_;
299              
300             # Is this a CTCP request or reply?
301 34 100       226 my $ctcp_type = $line->{command} eq 'PRIVMSG' ? 'ctcp' : 'ctcpreply';
302              
303             # CAPAP IDENTIFY-MSG is only applied to ACTIONs
304 34         118 my ($msg, $identified) = ($line->{params}->[1], undef);
305 34 50 33     144 ($msg, $identified) = _split_idmsg($msg) if $self->{identifymsg} && $msg =~ /^.ACTION/;
306              
307 34         77 my $events = [ ];
308 34         118 my ($ctcp, $text) = _ctcp_dequote($msg);
309              
310 34 100       103 if (!defined $ctcp) {
311 1 50       3 warn "Received malformed CTCP message: $msg\n" if $self->{debug};
312 1         6 return $events;
313             }
314              
315 33 100       173 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         69 my ($name, $args);
321 33         112 CTCP: for my $string ($ctcp->[0]) {
322 33 50       244 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       127 if (lc $name eq 'dcc') {
331 11         27 my ($dcc_type, $rest);
332              
333 11 50       94 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         33 $dcc_type = uc $dcc_type;
342              
343 11         101 my ($handler) = grep { $dcc_type =~ /$_/ } keys %dcc_types;
  22         650  
344 11 50       50 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         61 my @dcc_args = $dcc_types{$handler}->($nick, $dcc_type, $rest);
350 11 50       45 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         123 };
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       203 };
    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         179 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   54 my ($line) = @_;
402 27         303 my %enquote = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");
403              
404 27 50       95 if (!defined $line) {
405 0         0 croak 'Not enough arguments to POE::Filter::IRC::Compat->_low_quote';
406             }
407              
408 27 50       86 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         98 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   83 my ($line) = @_;
419 34         312 my %dequote = (n => "\012", r => "\015", 0 => "\0", "\cP" => "\cP");
420              
421 34 50       108 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       102 if ($line =~ tr/\cP//) {
428 0         0 $line =~ s/\cP([nr0\cP])/$dequote{$1}/g;
429             }
430              
431 34         120 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