File Coverage

blib/lib/POE/Filter/IRCv3.pm
Criterion Covered Total %
statement 135 138 97.8
branch 50 62 80.6
condition 15 21 71.4
subroutine 17 17 100.0
pod 10 10 100.0
total 227 248 91.5


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