File Coverage

blib/lib/POE/Filter/IRCv3.pm
Criterion Covered Total %
statement 143 146 97.9
branch 50 62 80.6
condition 14 19 73.6
subroutine 17 17 100.0
pod 10 10 100.0
total 234 254 92.1


line stmt bran cond sub pod time code
1             package POE::Filter::IRCv3;
2             $POE::Filter::IRCv3::VERSION = '1.001001';
3 2     2   259323 use strict; use warnings FATAL => 'all';
  2     2   4  
  2         105  
  2         12  
  2         5  
  2         106  
4              
5 2     2   10 use Carp;
  2         5  
  2         242  
6              
7             BEGIN {
8 2 50   2   5 if (eval { require POE::Filter; 1 }) {
  2         1984  
  2         1485  
9 2         1648 our @ISA = 'POE::Filter';
10             }
11             }
12              
13             =pod
14              
15             =for Pod::Coverage COLONIFY DEBUG BUFFER SPCHR
16              
17             =cut
18              
19             sub COLONIFY () { 0 }
20             sub DEBUG () { 1 }
21             sub BUFFER () { 2 }
22             sub SPCHR () { "\x20" }
23              
24             our %CharToEscapedTag = (
25             ';' => '\:',
26             ' ' => '\s',
27             "\0" => '\0',
28             "\\" => '\\',
29             "\r" => '\r',
30             "\n" => '\n',
31             "\a" => '\a',
32             );
33              
34             our %EscapedTagToChar = reverse %CharToEscapedTag;
35              
36             sub new {
37 4     4 1 14061 my ($class, %params) = @_;
38 4         16 map {; $params{uc $_} = $params{$_} } keys %params;
  3         15  
39 4   100     119 bless [
      50        
40             ($params{'COLONIFY'} || 0),
41             ($params{'DEBUG'} || $ENV{POE_FILTER_IRC_DEBUG} || 0),
42             [] ## BUFFER
43             ], $class
44             }
45              
46             sub clone {
47 1     1 1 2 my ($self) = @_;
48 1         2 my $nself = [@$self];
49 1         2 $nself->[BUFFER] = [];
50 1         4 bless $nself, ref $self
51             }
52              
53             sub debug {
54 4     4 1 910 my ($self, $value) = @_;
55 4 100       16 return $self->[DEBUG] = $value if defined $value;
56 2         9 $self->[DEBUG]
57             }
58              
59             sub colonify {
60 2     2 1 707 my ($self, $value) = @_;
61 2 100       14 return $self->[COLONIFY] = $value if defined $value;
62 1         3 $self->[COLONIFY]
63             }
64              
65              
66             sub get_one_start {
67 2     2 1 340 my ($self, $raw_lines) = @_;
68 2         5 push @{ $self->[BUFFER] }, $_ for @$raw_lines;
  2         8  
69             }
70              
71             sub get_pending {
72 1     1 1 3 my ($self) = @_;
73 1 50       2 @{ $self->[BUFFER] } ? [ @{ $self->[BUFFER] } ] : ()
  1         4  
  1         4  
74             }
75              
76             sub get {
77 39     39 1 127350 my @events;
78 39         85 for my $raw_line (@{ $_[1] }) {
  39         125  
79 39 50       210 warn " >> '$raw_line'\n" if $_[0]->[DEBUG];
80 39 100       101 if ( my $event = parse_one_line($raw_line) ) {
81 36         121 push @events, $event;
82             } else {
83 3         488 carp "Received malformed IRC input: $raw_line";
84             }
85             }
86             \@events
87 39         317 }
88              
89             sub get_one {
90 2     2 1 1578 my ($self) = @_;
91 2         3 my @events;
92 2 50       4 if ( my $raw_line = shift @{ $self->[BUFFER] } ) {
  2         9  
93 2 50       5 warn " >> '$raw_line'\n" if $self->[DEBUG];
94 2 50       7 if ( my $event = parse_one_line($raw_line) ) {
95 2         4 push @events, $event;
96             } else {
97 0         0 warn "Received malformed IRC input: $raw_line\n";
98             }
99             }
100             \@events
101 2         9 }
102              
103              
104 2     2   2401 use bytes;
  2         112  
  2         13  
105 2     2   81 no warnings 'substr';
  2         5  
  2         2444  
106              
107             sub put {
108 21     21 1 613630 my ($self, $events) = @_;
109 21         48 my $raw_lines = [];
110              
111 21         57 for my $event (@$events) {
112              
113 21 50       73 if ( ref $event eq 'HASH' ) {
114 21         27 my $raw_line;
115              
116             ## FIXME this gets glacially slow ->
117 21 100 66     106 if ( exists $event->{tags} && (my @tags = %{ $event->{tags} }) ) {
  4         34  
118 4         8 $raw_line .= '@';
119 4         20 while (my ($thistag, $thisval) = splice @tags, 0, 2) {
120 8         11 $raw_line .= $thistag;
121 8 100       38 if (defined $thisval) {
122 6         8 $raw_line .= '=';
123 6         10 my $tag_pos = 0;
124 6         5 my $len = length $thisval;
125 6         37 while ($tag_pos < $len) {
126 40         63 my $ch = substr $thisval, $tag_pos++, 1;
127 40 100       101 $raw_line .= exists $CharToEscapedTag{$ch} ?
128             $CharToEscapedTag{$ch} : $ch
129             }
130             }
131 8 100       40 $raw_line .= ';' if @tags;
132             }
133 4         8 $raw_line .= ' ';
134             }
135              
136 21 100       91 $raw_line .= ':' . $event->{prefix} . ' ' if $event->{prefix};
137 21         48 $raw_line .= $event->{command};
138              
139 21 100 66     141 if ( $event->{params} && (my @params = @{ $event->{params} }) ) {
  17         125  
140 17         26 $raw_line .= ' ';
141 17         51 my $param = shift @params;
142 17         62 while (@params) {
143 14         30 $raw_line .= $param . ' ';
144 14         52 $param = shift @params;
145             }
146 17 100 66     139 $raw_line .= ':'
    100 66        
147             if (index($param, SPCHR) != -1)
148             or (index($param, ':') == 0)
149             or (
150             defined $event->{colonify} ?
151             $event->{colonify} : $self->[COLONIFY]
152             );
153 17         29 $raw_line .= $param;
154             }
155              
156 21         59 push @$raw_lines, $raw_line;
157 21 50       105 warn " << '$raw_line'\n" if $self->[DEBUG];
158             } else {
159 0         0 carp "($self) non-HASH passed to put(): '$event'";
160 0 0       0 push @$raw_lines, $event if ref $event eq 'SCALAR';
161             }
162              
163             }
164              
165             $raw_lines
166 21         73 }
167              
168              
169             sub parse_one_line {
170 42     42 1 819 my $raw_line = $_[0];
171 42         127 my %event = ( raw_line => $raw_line );
172 42         64 my $pos = 0;
173              
174             ## We cheat a little; the spec is fuzzy when it comes to CR, LF, and NUL
175             ## bytes. Theoretically they're not allowed inside messages, but
176             ## that's really an implementation detail (and the spec agrees).
177             ## We just stick to SPCHR (\x20) here.
178              
179 42 100       158 if ( substr($raw_line, 0, 1) eq '@' ) {
180 21 50       73 return unless (my $nextsp = index($raw_line, SPCHR)) > 0;
181             # Tag parser cheats and uses split, at the moment:
182 21         102 for ( split /;/, substr $raw_line, 1, ($nextsp - 1) ) {
183 41         100 my ($thistag, $thisval) = split /=/;
184 41         43 my $realval;
185 41 100       94 if (defined $thisval) {
186 28         35 my $tag_pos = 0;
187 28         35 my $len = length $thisval;
188 28         67 while ($tag_pos < $len) {
189 132         190 my $ch = substr $thisval, $tag_pos++, 1;
190 132 100       206 if ($ch eq "\\") {
191 15         25 my $pair = $ch . (substr $thisval, $tag_pos++, 1 || '');
192 15 100       33 if (exists $EscapedTagToChar{$pair}) {
193 13         41 $realval .= $EscapedTagToChar{$pair}
194             } else {
195 2         5 $realval .= substr $pair, 1, 1;
196             }
197             } else {
198 117         271 $realval .= $ch
199             }
200             }
201             }
202 41         143 $event{tags}->{$thistag} = $realval
203             }
204 21         44 $pos = $nextsp + 1;
205             }
206              
207 42         174 $pos++ while substr($raw_line, $pos, 1) eq SPCHR;
208              
209 42 100       114 if ( substr($raw_line, $pos, 1) eq ':' ) {
210 26         28 my $nextsp;
211 26 100 100     227 ($nextsp = index $raw_line, SPCHR, $pos) > 0 and length(
212             $event{prefix} = substr $raw_line, ($pos + 1), ($nextsp - $pos - 1)
213             ) or return;
214 23         36 $pos = $nextsp + 1;
215 23         70 $pos++ while substr($raw_line, $pos, 1) eq SPCHR;
216             }
217              
218 39         54 my $nextsp_maybe;
219 39 100       130 if ( ($nextsp_maybe = index $raw_line, SPCHR, $pos) == -1 ) {
220             # No more spaces; do we have anything..?
221 8         13 my $cmd = substr $raw_line, $pos;
222 8 50       35 $event{command} = uc( length $cmd ? $cmd : return );
223 8         36 return \%event
224             }
225              
226             $event{command} = uc(
227 31         124 substr($raw_line, $pos, ($nextsp_maybe - $pos) )
228             );
229 31         51 $pos = $nextsp_maybe + 1;
230              
231 31         101 $pos++ while substr($raw_line, $pos, 1) eq SPCHR;
232              
233 31         50 my $maxlen = length $raw_line;
234 31         75 PARAM: while ( $pos < $maxlen ) {
235 54 100       132 if ( substr($raw_line, $pos, 1) eq ':' ) {
236 21         29 push @{ $event{params} }, substr $raw_line, ($pos + 1);
  21         57  
237             last PARAM
238 21         74 }
239 33 100       106 if ( (my $nextsp = index $raw_line, SPCHR, $pos) == -1 ) {
240 9         13 push @{ $event{params} }, substr $raw_line, $pos;
  9         72  
241             last PARAM
242 9         20 } else {
243 24         27 push @{ $event{params} }, substr $raw_line, $pos, ($nextsp - $pos);
  24         83  
244 24         41 $pos = $nextsp + 1;
245 24         70 $pos++ while substr($raw_line, $pos, 1) eq SPCHR;
246             next PARAM
247 24         62 }
248             }
249              
250 31         118 \%event
251             }
252              
253              
254 2     2   14 no bytes;
  2         5  
  2         11  
255              
256              
257             print
258             qq[ let's try this again -without- the part where we beat you to],
259             qq[ death with a six foot plush toy of sexual harassment panda\n ]
260             unless caller; 1;
261              
262              
263             =pod
264              
265             =head1 NAME
266              
267             POE::Filter::IRCv3 - Fast IRCv3.2 parser for POE or stand-alone use
268              
269             =head1 SYNOPSIS
270              
271             my $filter = POE::Filter::IRCv3->new(colonify => 1);
272              
273             # Raw lines parsed to hashes:
274             my $array_of_refs = $filter->get(
275             [
276             ':prefix COMMAND foo :bar',
277             '@foo=bar;baz :prefix COMMAND foo :bar',
278             ]
279             );
280              
281             # Hashes deparsed to raw lines:
282             my $array_of_lines = $filter->put(
283             [
284             {
285             prefix => 'prefix',
286             command => 'COMMAND',
287             params => [
288             'foo',
289             'bar'
290             ],
291             },
292             {
293             prefix => 'prefix',
294             command => 'COMMAND',
295             params => [
296             'foo',
297             'bar'
298             ],
299             tags => {
300             foo => 'bar',
301             baz => undef,
302             },
303             },
304             ]
305             );
306              
307              
308             # Stacked with a line filter, suitable for Wheel usage, etc:
309             my $ircd = POE::Filter::IRCv3->new(colonify => 1);
310             my $line = POE::Filter::Line->new(
311             InputRegexp => '\015?\012',
312             OutputLiteral => "\015\012",
313             );
314             my $stacked = POE::Filter::Stackable->new(
315             Filters => [ $line, $ircd ],
316             );
317              
318             # Functional parser interface:
319             my $event = POE::Filter::IRCv3::parse_one_line(
320             ':foo PRIVMSG #bar :baz quux'
321             );
322              
323             =head1 DESCRIPTION
324              
325             A L for IRC traffic with support for IRCv3.2 message tags.
326              
327             Does not rely on regular expressions for parsing. Benchmarks show this
328             approach is generally faster on the most common IRC strings.
329              
330             Like any proper L, there are no POE-specific bits involved here
331             -- the filter can be used stand-alone to parse lines of IRC traffic (also see
332             L).
333              
334             In fact, you do not need L installed -- if L is not
335             available, it is left out of C<@ISA> and the filter will continue working
336             normally.
337              
338             =head2 POE / Object interface
339              
340             =head3 new
341              
342             Construct a new Filter; if the B option is true,
343             the last parameter will always have a colon prepended.
344             (This setting can also be retrieved or changed on-the-fly by calling
345             B as a method, or changed for specific events by passing a
346             B option via events passed to L.)
347              
348             =head3 get_one_start, get_one, get_pending
349              
350             Implement the interface described in L.
351              
352             See L.
353              
354             =head3 get
355              
356             my $events = $filter->get( [ $line, $another, ... ] );
357             for my $event (@$events) {
358             my $cmd = $event->{command};
359             ## See below for other keys available
360             }
361              
362             Takes an ARRAY of raw lines and returns an ARRAY of HASH-type references with
363             the following keys:
364              
365             =head4 command
366              
367             The (uppercased) command or numeric.
368              
369             =head4 params
370              
371             An ARRAY containing the event parameters.
372              
373             =head4 prefix
374              
375             The sender prefix, if any.
376              
377             =head4 tags
378              
379             A HASH of key => value pairs matching IRCv3.2 "message tags" -- see
380             L.
381              
382             Note that a tag can be present, but have an undefined value.
383              
384             =head3 put
385              
386             my $lines = $filter->put( [ $hash, $another_hash, ... ] );
387             for my $line (@$lines) {
388             ## Direct to socket, etc
389             }
390              
391             Takes an ARRAY of HASH-type references matching those described in L
392             (documented above) and returns an ARRAY of raw IRC-formatted lines.
393              
394             =head4 colonify
395              
396             In addition to the keys described in L, the B option can be
397             specified for specific events. This controls whether or not the last
398             parameter will be colon-prefixed even if it is a single word. (Yes, IRC is
399             woefully inconsistent ...)
400              
401             Specify as part of the event hash:
402              
403             $filter->put([ { %event, colonify => 1 } ]);
404              
405             =head3 clone
406              
407             Copy the filter object (with a cleared buffer).
408              
409             =head3 debug
410              
411             Turn on/off debug output, which will display every input/output line (and
412             possibly other data in the future).
413              
414             This is enabled by default at construction time if the environment variable
415             C is a true value.
416              
417             =head2 Functional interface
418              
419             =head3 parse_one_line
420              
421             If the filter is being used as a stand-alone IRC parser and speed is of the
422             essence, you can skip method resolution & queue handling by calling the parse
423             function directly using the fully-qualified name:
424              
425             my $ev = POE::Filter::IRCv3::parse_one_line( $line );
426              
427             The function takes a single line and returns a HASH whose structure is
428             described in the documentation for L, above.
429              
430             If the given line cannot be parsed, the function returns false (rather than
431             throwing an exception, as L would).
432              
433             There is currently no functional interface to message string composition
434             (L).
435              
436             =head1 AUTHOR
437              
438             Jon Portnoy
439              
440             Licensed under the same terms as Perl.
441              
442             Original implementations were derived from L,
443             which is copyright Chris Williams and Jonathan Steinert. This codebase has
444             diverged significantly.
445              
446             Major thanks to the C<#ircv3> crew on irc.atheme.org, especially C and
447             C, for various bits of inspiration.
448              
449             =head1 SEE ALSO
450              
451             L
452              
453             L
454              
455             L
456              
457             L
458              
459             L
460              
461             L
462              
463             =cut