File Coverage

blib/lib/Parse/IRC.pm
Criterion Covered Total %
statement 67 110 60.9
branch 22 82 26.8
condition 12 22 54.5
subroutine 9 11 81.8
pod 3 3 100.0
total 113 228 49.5


line stmt bran cond sub pod time code
1             package Parse::IRC;
2             $Parse::IRC::VERSION = '1.22';
3             #ABSTRACT: A parser for the IRC protocol.
4              
5             # We export some stuff
6             require Exporter;
7             @ISA = qw[Exporter];
8             @EXPORT = qw[parse_irc];
9              
10 3     3   39045 use strict;
  3         6  
  3         66  
11 3     3   8 use warnings;
  3         3  
  3         73  
12 3     3   9 use File::Basename qw[fileparse];
  3         3  
  3         4903  
13              
14             my $g = {
15             space => qr/\x20+/o,
16             trailing_space => qr/\x20*/o,
17             };
18              
19             my $irc_regex = qr/^
20             (?:
21             \x3a # : comes before hand
22             (\S+) # [prefix]
23             $g->{'space'} # Followed by a space
24             )? # but is optional.
25             (
26             \d{3}|[a-zA-Z]+ # [command]
27             ) # required.
28             (?:
29             $g->{'space'} # Strip leading space off [middle]s
30             ( # [middle]s
31             (?:
32             [^\x00\x0a\x0d\x20\x3a]
33             [^\x00\x0a\x0d\x20]*
34             ) # Match on 1 of these,
35             (?:
36             $g->{'space'}
37             [^\x00\x0a\x0d\x20\x3a]
38             [^\x00\x0a\x0d\x20]*
39             )* # then match on 0-13 of these,
40             )
41             )? # otherwise dont match at all.
42             (?:
43             $g->{'space'}\x3a # Strip off leading spacecolon for [trailing]
44             ([^\x00\x0a\x0d]*) # [trailing]
45             )? # [trailing] is not necessary.
46             $g->{'trailing_space'}
47             $/x;
48              
49             # the magic cookie jar
50             my %dcc_types = (
51             qr/^(?:CHAT|SEND)$/ => sub {
52             my ($nick, $type, $args) = @_;
53             my ($file, $addr, $port, $size);
54             return if !(($file, $addr, $port, $size) = $args =~ /^(".+"|[^ ]+) +([0-9a-fA-F:]+) +(\d+)(?: +(\d+))?/);
55              
56             if ($file =~ s/^"//) {
57             $file =~ s/"$//;
58             $file =~ s/\\"/"/g;
59             }
60             $file = fileparse($file);
61              
62             return (
63             $port,
64             {
65             nick => $nick,
66             type => $type,
67             file => $file,
68             size => $size,
69             addr => $addr,
70             port => $port,
71             },
72             $file,
73             $size,
74             $addr,
75             );
76             },
77             qr/^(?:ACCEPT|RESUME)$/ => sub {
78             my ($nick, $type, $args) = @_;
79             my ($file, $port, $position);
80             return if !(($file, $port, $position) = $args =~ /^(".+"|[^ ]+) +(\d+) +(\d+)/);
81              
82             $file =~ s/^"|"$//g;
83             $file = fileparse($file);
84              
85             return (
86             $port,
87             {
88             nick => $nick,
89             type => $type,
90             file => $file,
91             size => $position,
92             port => $port,
93             },
94             $file,
95             $position,
96             );
97             },
98             );
99              
100             sub parse_irc {
101 2   50 2 1 16 my $string = shift || return;
102 2         10 return __PACKAGE__->new(@_)->parse($string);
103             }
104              
105             sub new {
106 7     7 1 4404 my $package = shift;
107 7         14 my %opts = @_;
108 7         27 $opts{lc $_} = delete $opts{$_} for keys %opts;
109 7         18 return bless \%opts, $package;
110             }
111              
112             sub parse {
113 8     8 1 1469 my $self = shift;
114 8   50     17 my $raw_line = shift || return;
115 8         29 $raw_line =~ s/(\x0D\x0A?|\x0A\x0D?)$//;
116 8 50       86 if ( my($prefix, $command, $middles, $trailing) = $raw_line =~ m/$irc_regex/ ) {
117 8         16 my $event = { raw_line => $raw_line };
118 8 50       21 $event->{'prefix'} = $prefix if $prefix;
119 8         12 $event->{'command'} = uc $command;
120 8 50 33     28 $event->{'params'} = [] if ( defined ( $middles ) || defined ( $trailing ) );
121 8 50       17 push @{$event->{'params'}}, (split /$g->{'space'}/, $middles) if defined ( $middles );
  8         34  
122 8 50       19 push @{$event->{'params'}}, $trailing if defined( $trailing );
  8         11  
123 8 50 66     39 if ( $self->{ctcp} and $event->{'command'} =~ /^(PRIVMSG|NOTICE)$/ and $event->{params}->[1] =~ tr/\001// ) {
      66        
124 2         5 return $self->_get_ctcp( $event );
125             }
126 6 50 66     22 if ( $self->{public} and $event->{'command'} eq 'PRIVMSG' and $event->{'params'}->[0] =~ /^(\x23|\x26)/ ) {
      66        
127 1         3 $event->{'command'} = 'PUBLIC';
128             }
129 6         11 return $event;
130             }
131             else {
132 0 0       0 warn "Received line $raw_line that is not IRC protocol\n" if $self->{debug};
133             }
134 0         0 return;
135             }
136              
137             sub _get_ctcp {
138 2     2   2 my ($self, $line) = @_;
139              
140             # Is this a CTCP request or reply?
141 2 50       4 my $ctcp_type = $line->{command} eq 'PRIVMSG' ? 'CTCP' : 'CTCPREPLY';
142              
143             # CAPAP IDENTIFY-MSG is only applied to ACTIONs
144 2         3 my ($msg, $identified) = ($line->{params}->[1], undef);
145 2 50 33     5 ($msg, $identified) = _split_idmsg($msg) if $self->{identifymsg} && $msg =~ /^.ACTION/;
146              
147 2         2 my $events;
148 2         3 my ($ctcp, $text) = _ctcp_dequote($msg);
149              
150 2 50       7 if (!defined $ctcp) {
151 0 0       0 warn "Received malformed CTCP message: $msg\n" if $self->{debug};
152 0         0 return $events;
153             }
154              
155 2 50       7 my $nick = defined $line->{prefix} ? (split /!/, $line->{prefix})[0] : undef;
156              
157             # We only process the first CTCP. The only people who send multiple ones
158             # are those who are trying to flood our outgoing queue anyway (e.g. by
159             # having us reply to 20 VERSION requests at a time).
160 2         2 my ($name, $args);
161 2         3 CTCP: for my $string ($ctcp->[0]) {
162 2 50       9 if (!(($name, $args) = $string =~ /^(\w+)(?: +(.*))?/)) {
163             defined $nick
164 0 0       0 ? do { warn "Received malformed CTCP message from $nick: $string\n" if $self->{debug} }
165 0 0       0 : do { warn "Trying to send malformed CTCP message: $string\n" if $self->{debug} }
  0 0       0  
166             ;
167 0         0 last CTCP;
168             }
169              
170 2 50       4 if (lc $name eq 'dcc') {
171 0         0 my ($dcc_type, $rest);
172              
173 0 0       0 if (!(($dcc_type, $rest) = $args =~ /^(\w+) +(.+)/)) {
174             defined $nick
175 0 0       0 ? do { warn "Received malformed DCC request from $nick: $args\n" if $self->{debug} }
176 0 0       0 : do { warn "Trying to send malformed DCC request: $args\n" if $self->{debug} }
  0 0       0  
177             ;
178 0         0 last CTCP;
179              
180             }
181 0         0 $dcc_type = uc $dcc_type;
182              
183 0         0 my ($handler) = grep { $dcc_type =~ /$_/ } keys %dcc_types;
  0         0  
184 0 0       0 if (!$handler) {
185 0 0       0 warn "Unhandled DCC $dcc_type request: $rest\n" if $self->{debug};
186 0         0 last CTCP;
187             }
188              
189 0         0 my @dcc_args = $dcc_types{$handler}->($nick, $dcc_type, $rest);
190 0 0       0 if (!@dcc_args) {
191             defined $nick
192 0 0       0 ? do { warn "Received malformed DCC $dcc_type request from $nick: $rest\n" if $self->{debug} }
193 0 0       0 : do { warn "Trying to send malformed DCC $dcc_type request: $rest\n" if $self->{debug} }
  0 0       0  
194             ;
195 0         0 last CTCP;
196             }
197              
198             $events = {
199             prefix => $line->{prefix},
200             command => 'DCC_REQUEST',
201             params => [
202             $dcc_type,
203             @dcc_args,
204             ],
205             raw_line => $line->{raw_line},
206 0         0 };
207             }
208             else {
209             $events = {
210             command => $ctcp_type . '_' . uc $name,
211             prefix => $line->{prefix},
212             params => [
213             $line->{params}->[0],
214             (defined $args ? $args : ''),
215             (defined $identified ? $identified : () ),
216             ],
217             raw_line => $line->{raw_line},
218 2 50       13 };
    50          
219             }
220             }
221              
222             # XXX: I'm not quite sure what this is for, but on FreeNode it adds an
223             # extra bogus event and displays a debug message, so I have disabled it.
224             # FreeNode precedes PRIVMSG and CTCP ACTION messages with '+' or '-'.
225             #if ($text && @$text) {
226             # my $what;
227             # ($what) = $line->{raw_line} =~ /^(:[^ ]+ +\w+ +[^ ]+ +)/
228             # or warn "What the heck? '".$line->{raw_line}."'\n" if $self->{debug};
229             # $text = (defined $what ? $what : '') . ':' . join '', @$text;
230             # $text =~ s/\cP/^P/g;
231             # warn "CTCP: $text\n" if $self->{debug};
232             # push @$events, @{ $self->{_ircd}->get([$text]) };
233             #}
234              
235 2         8 return $events;
236             }
237              
238             sub _split_idmsg {
239 0     0   0 my ($line) = @_;
240 0         0 my ($identified, $msg) = split //, $line, 2;
241 0 0       0 $identified = $identified eq '+' ? 1 : 0;
242 0         0 return $msg, $identified;
243             }
244              
245             # Splits a message into CTCP and text chunks. This is gross. Most of
246             # this is also stolen from Net::IRC, but I (fimm) wrote that too, so it's
247             # used with permission. ;-)
248             sub _ctcp_dequote {
249 2     2   3 my ($msg) = @_;
250 2         1 my (@chunks, $ctcp, $text);
251              
252             # CHUNG! CHUNG! CHUNG!
253              
254 2 50       4 if (!defined $msg) {
255 0         0 die 'Not enough arguments to Parse::IRC::_ctcp_dequote';
256             }
257              
258             # Strip out any low-level quoting in the text.
259 2         3 $msg = _low_dequote( $msg );
260              
261             # Filter misplaced \001s before processing... (Thanks, tchrist!)
262 2 50       6 substr($msg, rindex($msg, "\001"), 1, '\\a')
263             if ($msg =~ tr/\001//) % 2 != 0;
264              
265 2 50       8 return if $msg !~ tr/\001//;
266              
267 2         5 @chunks = split /\001/, $msg;
268 2 50       5 shift @chunks if !length $chunks[0]; # FIXME: Is this safe?
269              
270 2         3 for (@chunks) {
271             # Dequote unnecessarily quoted chars, and convert escaped \'s and ^A's.
272 2         3 s/\\([^\\a])/$1/g;
273 2         12 s/\\\\/\\/g;
274 2         4 s/\\a/\001/g;
275             }
276              
277             # If the line begins with a control-A, the first chunk is a CTCP
278             # message. Otherwise, it starts with text and alternates with CTCP
279             # messages. Really stupid protocol.
280 2 50       7 if ($msg =~ /^\001/) {
281 2         5 push @$ctcp, shift @chunks;
282             }
283              
284 2         4 while (@chunks) {
285 0         0 push @$text, shift @chunks;
286 0 0       0 push @$ctcp, shift @chunks if @chunks;
287             }
288              
289 2         3 return ($ctcp, $text);
290             }
291              
292             # Quotes a string in a low-level, protocol-safe, utterly brain-dead
293             # fashion. Returns the quoted string.
294             sub _low_quote {
295 0     0   0 my ($line) = @_;
296 0         0 my %enquote = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");
297              
298 0 0       0 if (!defined $line) {
299 0         0 die 'Not enough arguments to Parse::IRC->_low_quote';
300             }
301              
302 0 0       0 if ($line =~ tr/[\012\015\0\cP]//) { # quote \n, \r, ^P, and \0.
303 0         0 $line =~ s/([\012\015\0\cP])/\cP$enquote{$1}/g;
304             }
305              
306 0         0 return $line;
307             }
308              
309             # Does low-level dequoting on CTCP messages. I hate this protocol.
310             # Yes, I copied this whole section out of Net::IRC.
311             sub _low_dequote {
312 2     2   2 my ($line) = @_;
313 2         6 my %dequote = (n => "\012", r => "\015", 0 => "\0", "\cP" => "\cP");
314              
315 2 50       3 if (!defined $line) {
316 0         0 die 'Not enough arguments to Parse::IRC->_low_dequote';
317             }
318              
319             # dequote \n, \r, ^P, and \0.
320             # Thanks to Abigail (abigail@foad.org) for this clever bit.
321 2 50       5 if ($line =~ tr/\cP//) {
322 0         0 $line =~ s/\cP([nr0\cP])/$dequote{$1}/g;
323             }
324              
325 2         3 return $line;
326             }
327              
328             q[Operation Blackbriar];
329              
330             __END__