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