File Coverage

blib/lib/AnyEvent/IRC/Util.pm
Criterion Covered Total %
statement 52 123 42.2
branch 14 48 29.1
condition 5 12 41.6
subroutine 11 21 52.3
pod 13 18 72.2
total 95 222 42.7


line stmt bran cond sub pod time code
1             package AnyEvent::IRC::Util;
2 2     2   33786 use common::sense;
  2         4  
  2         17  
3 2     2   105 use Exporter;
  2         2  
  2         176  
4 2     2   2622 use Encode;
  2         30408  
  2         6391  
5             our @ISA = qw/Exporter/;
6             our @EXPORT_OK =
7             qw(mk_msg parse_irc_msg split_prefix prefix_nick
8             decode_ctcp encode_ctcp filter_ctcp_text_attr prefix_user prefix_host
9             rfc_code_to_name filter_colors is_nick_prefix join_prefix
10             split_unicode_string);
11              
12             =head1 NAME
13              
14             AnyEvent::IRC::Util - Common utilities that help with IRC protocol handling
15              
16             =head1 SYNOPSIS
17              
18             use AnyEvent::IRC::Util qw/parse_irc_msg mk_msg/;
19              
20             my $msgdata = mk_msg (undef, PRIVMSG => "mcmanus", "my hands glow!");
21              
22             =head1 FUNCTIONS
23              
24             These are some utility functions that might come in handy when
25             handling the IRC protocol.
26              
27             You can export these with eg.:
28              
29             use AnyEvent::IRC::Util qw/parse_irc_msg/;
30              
31             =over 4
32              
33             =item B<parse_irc_msg ($ircline)>
34              
35             This method parses the C<$ircline>, which is one line of the IRC protocol
36             without the trailing "\015\012".
37              
38             It returns a hash which has the following entrys:
39              
40             =over 4
41              
42             =item prefix
43              
44             The message prefix.
45              
46             =item command
47              
48             The IRC command.
49              
50             =item params
51              
52             The parameters to the IRC command in a array reference,
53             this includes the trailing parameter (the one after the ':' or
54             the 14th parameter).
55              
56             =back
57              
58             =cut
59              
60             sub parse_irc_msg {
61 10     10 1 7907 my ($msg) = @_;
62              
63 10 50       69 $msg =~ s/^(?::([^ ]+)[ ])?([A-Za-z]+|\d{3})//
64             or return undef;
65 10         14 my %msg;
66 10         48 ($msg{prefix}, $msg{command}, $msg{params}) = ($1, $2, []);
67              
68 10         18 my $cnt = 0;
69 10         44 while ($msg =~ s/^[ ]([^ :\015\012\0][^ \015\012\0]*)//) {
70 8 50       23 push @{$msg{params}}, $1 if defined $1;
  8         23  
71 8 50       39 last if ++$cnt > 13;
72             }
73              
74 10 50       18 if ($cnt == 14) {
75 0 0       0 if ($msg =~ s/^[ ]:?([^\015\012\0]*)//) {
76 0 0       0 push @{$msg{params}}, $1 if defined $1;
  0         0  
77             }
78              
79             } else {
80 10 100       37 if ($msg =~ s/^[ ]:([^\015\012\0]*)//) {
81 8 50       18 push @{$msg{params}}, $1 if defined $1;
  8         20  
82             }
83             }
84              
85 10         29 \%msg
86             }
87              
88             =item B<mk_msg ($prefix, $command, @params)>
89              
90             This function assembles a IRC message. The generated
91             message will look like (pseudo code!)
92              
93             :<prefix> <command> <params> :<trail>
94              
95             Please refer to RFC 1459 how IRC messages normally look like.
96              
97             The prefix will be omitted if they are C<undef>.
98              
99             Please note that only the last parameter may contain spaces, and if it
100             contains spaces it will be quoted as the trailing part of the
101             IRC message.
102              
103             NOTE: The trailing "\015\012" is NOT added by this function!
104              
105             EXAMPLES:
106              
107             mk_msg (undef, "PRIVMSG", "magnus", "you suck!");
108             # will return: "PRIVMSG magnus :you suck!"
109              
110             mk_msg (undef, "PRIVMSG", "magnus", "Hi!");
111             # will return: "PRIVMSG magnus :Hi!"
112              
113             mk_msg (undef, "JOIN", "#test");
114             # will return: "JOIN #test"
115              
116             =cut
117              
118             sub mk_msg {
119 4     4 1 33 my ($prefix, $command, @params) = @_;
120 4         6 my $msg = "";
121              
122 4 100       14 $msg .= defined $prefix ? ":$prefix " : "";
123 4         6 $msg .= "$command";
124              
125 4         5 my $trail;
126 4 50 66     30 if (@params && ($params[-1] =~ /\x20/ || $params[-1] =~ /^:/)) {
      66        
127 3         5 $trail = pop @params;
128             }
129              
130             # FIXME: params must be counted, and if > 13 they have to be
131             # concationated with $trail
132 4         7 map { $msg .= " $_" } @params;
  3         9  
133              
134 4 100       12 $msg .= defined $trail ? " :$trail" : "";
135              
136 4         12 return $msg;
137             }
138              
139             my @_ctcp_lowlevel_escape = ("\000", "0", "\012", "n", "\015", "r", "\020", "\020");
140              
141             sub unescape_lowlevel {
142 0     0 0 0 my ($data) = @_;
143 0         0 my %map = reverse @_ctcp_lowlevel_escape;
144 0 0       0 $data =~ s/\020(.)/defined $map{$1} ? $map{$1} : $1/ge;
  0         0  
145 0         0 $data
146             }
147              
148             sub escape_lowlevel {
149 0     0 0 0 my ($data) = @_;
150 0         0 my %map = @_ctcp_lowlevel_escape;
151 0         0 $data =~ s/([\000\012\015\020])/"\020$map{$1}"/ge;
  0         0  
152 0         0 $data
153             }
154              
155             sub unescape_ctcp {
156 0     0 0 0 my ($data) = @_;
157 0 0       0 $data =~ s/\\(.)/$1 eq 'a' ? "\001" : ($1 eq "\\" ? "\\" : $1)/eg;
  0 0       0  
158 0         0 $data
159             }
160              
161             sub escape_ctcp {
162 0     0 0 0 my ($data) = @_;
163 0 0       0 $data =~ s/([\\\001])/$1 eq "\001" ? "\\a" : "\\\\"/eg;
  0         0  
164 0         0 $data
165             }
166              
167             =item B<decode_ctcp ($data)>
168              
169             This function decodes CTCP messages contained in an IRC message.
170             C<$data> should be the last parameter of a IRC PRIVMSG or NOTICE.
171              
172             It will first unescape the lower layer, extract CTCP messages
173             and then return a list with two elements: the line without the CTCP messages
174             and an array reference which contains array references of CTCP messages.
175             Those CTCP message array references will have the CTCP message tag as
176             first element (eg. "VERSION") and the rest of the CTCP message as the second
177             element.
178              
179             =cut
180              
181             sub decode_ctcp {
182 0     0 1 0 my ($line) = @_;
183              
184 0         0 $line = unescape_lowlevel ($line);
185 0         0 my @ctcp;
186 0         0 while ($line =~ /\G\001([^\001]*)\001/g) {
187 0         0 my $msg = unescape_ctcp ($1);
188 0         0 my ($tag, $data) = split / /, $msg, 2;
189 0         0 push @ctcp, [$tag, $data];
190             }
191              
192 0         0 $line =~ s/\001[^\001]*\001//g;
193              
194             # try to parse broken ctcp messages anyway
195 0 0       0 if ($line =~ s/\001([^\001]*)$//) {
196 0         0 my $msg = unescape_ctcp ($1);
197 0         0 my ($tag, $data) = split / /, $msg, 2;
198 0         0 push @ctcp, [$tag, $data];
199             }
200              
201 0         0 return ($line, \@ctcp)
202             }
203              
204             =item B<encode_ctcp (@msg)>
205              
206             This function encodes a CTCP message for the transmission via the NOTICE
207             or PRIVMSG command. C<@msg> is an array of strings or array references.
208             If an array reference occurs in the C<@msg> array it's first
209             element will be interpreted as CTCP TAG (eg. one of PING, VERSION, .. whatever)
210             the rest of the array ref will be appended to the tag and separated by
211             spaces.
212              
213             All parts of the message will be concatenated and lowlevel quoted.
214             That means you can embed _any_ character from 0 to 255 in this message (thats
215             what the lowlevel quoting allows).
216              
217             =cut
218              
219             sub encode_ctcp {
220 0     0 1 0 my (@args) = @_;
221 0 0       0 escape_lowlevel (
222             join "", map {
223 0         0 ref $_
224             ? "\001" . escape_ctcp (join " ", @$_) . "\001"
225             : $_
226             } @args
227             )
228             }
229              
230             =item B<filter_colors ($line)>
231              
232             This function will filter out any mIRC colors and (most) ansi escape sequences.
233             Unfortunately the mIRC color coding will destroy improper colored numbers. So this
234             function may destroy the message in some occasions a bit.
235              
236             =cut
237              
238             sub filter_colors($) {
239 3     3 1 440 my ($line) = @_;
240 3         18 $line =~ s/\x1B\[.*?[\x00-\x1F\x40-\x7E]//g; # see ECMA-48 + advice by urxvt author
241 3         17 $line =~ s/\x03\d\d?(?:,\d\d?)?//g; # see http://www.mirc.co.uk/help/color.txt
242 3         8 $line =~ s/[\x03\x16\x02\x1f\x0f]//g; # see some undefined place :-)
243 3         35 $line
244             }
245              
246              
247             # implemented after the below CTCP spec, but
248             # doesnt seem to be used by anyone... so it's untested.
249             sub filter_ctcp_text_attr_bogus {
250 0     0 0 0 my ($line, $cb) = @_;
251 0 0       0 return unless $cb;
252 0 0       0 $line =~ s/\006([BVUSI])/{warn "FIL\n"; my $c = $cb->($1); defined $c ? $c : "\006$1"}/ieg;
  0         0  
  0         0  
  0         0  
  0         0  
253 0 0       0 $line =~ s/\006CA((?:I[0-9A-F]|#[0-9A-F]{3}){2})/{my $c = $cb->($1); defined $c ? $c : "\006CA$1"}/ieg;
  0         0  
  0         0  
  0         0  
254 0 0       0 $line =~ s/\006C([FB])(I[0-9A-F]|#[0-9A-F]{3})/{my $c = $cb->($1, $2); defined $c ? $c : "\006C$1$2"}/ieg;
  0         0  
  0         0  
  0         0  
255 0 0       0 $line =~ s/\006CX([AFB])/{my $c = $cb->($1); defined $c ? $c : "\006CX$1"}/ieg;
  0         0  
  0         0  
  0         0  
256 0         0 return $line;
257             }
258              
259             =item B<split_prefix ($prefix)>
260              
261             This function splits an IRC user prefix as described by RFC 2817
262             into the three parts: nickname, user and host. Which will be
263             returned as a list with that order.
264              
265             C<$prefix> can also be a hash like it is returned by C<parse_irc_msg>.
266              
267             =cut
268              
269             sub split_prefix {
270 8     8 1 17 my ($prfx) = @_;
271              
272 8 100       22 if (ref ($prfx) eq 'HASH') {
273 6         12 $prfx = $prfx->{prefix};
274             }
275              
276             # this splitting does indeed use the servername as nickname, but there
277             # is no way for a client to distinguish.
278 8         44 $prfx =~ m/^\s*([^!]*)(?:!([^@]*))?(?:@(.*?))?\s*$/;
279 8         53 return ($1, $2, $3);
280             }
281              
282             =item B<is_nick_prefix ($prefix)>
283              
284             Returns true if the prefix is a nick prefix, containing user and host.
285              
286             =cut
287              
288             sub is_nick_prefix {
289 0     0 1 0 my ($prfx) = @_;
290 0         0 $prfx =~ m/^\s*([^!]+)!([^@]+)@(.+)?\s*$/;
291             }
292              
293             =item B<join_prefix ($nick, $user, $host)>
294              
295             Joins C<$nick>, C<$user> and C<$host> together to form a prefix.
296              
297             =cut
298              
299             sub join_prefix {
300 0     0 1 0 my ($n, $u, $h) = @_;
301 0         0 "$n!$u\@$h"
302             }
303              
304             =item B<prefix_nick ($prefix)>
305              
306             A shortcut to extract the nickname from the C<$prefix>.
307              
308             C<$prefix> can also be a hash like it is returned by C<parse_irc_msg>.
309              
310             =cut
311              
312             sub prefix_nick {
313 2     2 1 2679 my ($prfx) = @_;
314 2         6 return (split_prefix ($prfx))[0];
315             }
316              
317             =item B<prefix_user ($prefix)>
318              
319             A shortcut to extract the username from the C<$prefix>.
320              
321             C<$prefix> can also be a hash like it is returned by C<parse_irc_msg>.
322              
323             =cut
324              
325             sub prefix_user {
326 2     2 1 3 my ($prfx) = @_;
327 2         5 return (split_prefix ($prfx))[1];
328             }
329              
330             =item B<prefix_host ($prefix)>
331              
332             A shortcut to extract the hostname from the C<$prefix>.
333              
334             C<$prefix> can also be a hash like it is returned by C<parse_irc_msg>.
335              
336             =cut
337              
338             sub prefix_host {
339 2     2 1 4 my ($prfx) = @_;
340 2         6 return (split_prefix ($prfx))[2];
341             }
342              
343              
344             =item B<rfc_code_to_name ($code)>
345              
346             This function is a interface to the internal mapping or numeric
347             replies to the reply name in RFC 2812 (which you may also consult).
348              
349             C<$code> is returned if no name for C<$code> exists
350             (as some server may extended the protocol).
351              
352             =cut
353              
354             our %RFC_NUMCODE_MAP = (
355             '001' => 'RPL_WELCOME',
356             '002' => 'RPL_YOURHOST',
357             '003' => 'RPL_CREATED',
358             '004' => 'RPL_MYINFO',
359             '005' => 'RPL_BOUNCE',
360             '200' => 'RPL_TRACELINK',
361             '201' => 'RPL_TRACECONNECTING',
362             '202' => 'RPL_TRACEHANDSHAKE',
363             '203' => 'RPL_TRACEUNKNOWN',
364             '204' => 'RPL_TRACEOPERATOR',
365             '205' => 'RPL_TRACEUSER',
366             '206' => 'RPL_TRACESERVER',
367             '207' => 'RPL_TRACESERVICE',
368             '208' => 'RPL_TRACENEWTYPE',
369             '209' => 'RPL_TRACECLASS',
370             '210' => 'RPL_TRACERECONNECT',
371             '211' => 'RPL_STATSLINKINFO',
372             '212' => 'RPL_STATSCOMMANDS',
373             '219' => 'RPL_ENDOFSTATS',
374             '221' => 'RPL_UMODEIS',
375             '233' => 'RPL_SERVICE',
376             '234' => 'RPL_SERVLIST',
377             '235' => 'RPL_SERVLISTEND',
378             '242' => 'RPL_STATSUPTIME',
379             '243' => 'RPL_STATSOLINE',
380             '250' => 'RPL_STATSDLINE',
381             '251' => 'RPL_LUSERCLIENT',
382             '252' => 'RPL_LUSEROP',
383             '253' => 'RPL_LUSERUNKNOWN',
384             '254' => 'RPL_LUSERCHANNELS',
385             '255' => 'RPL_LUSERME',
386             '256' => 'RPL_ADMINME',
387             '257' => 'RPL_ADMINLOC1',
388             '258' => 'RPL_ADMINLOC2',
389             '259' => 'RPL_ADMINEMAIL',
390             '261' => 'RPL_TRACELOG',
391             '262' => 'RPL_TRACEEND',
392             '263' => 'RPL_TRYAGAIN',
393             '301' => 'RPL_AWAY',
394             '302' => 'RPL_USERHOST',
395             '303' => 'RPL_ISON',
396             '305' => 'RPL_UNAWAY',
397             '306' => 'RPL_NOWAWAY',
398             '311' => 'RPL_WHOISUSER',
399             '312' => 'RPL_WHOISSERVER',
400             '313' => 'RPL_WHOISOPERATOR',
401             '314' => 'RPL_WHOWASUSER',
402             '315' => 'RPL_ENDOFWHO',
403             '317' => 'RPL_WHOISIDLE',
404             '318' => 'RPL_ENDOFWHOIS',
405             '319' => 'RPL_WHOISCHANNELS',
406             '321' => 'RPL_LISTSTART',
407             '322' => 'RPL_LIST',
408             '323' => 'RPL_LISTEND',
409             '324' => 'RPL_CHANNELMODEIS',
410             '325' => 'RPL_UNIQOPIS',
411             '331' => 'RPL_NOTOPIC',
412             '332' => 'RPL_TOPIC',
413             '341' => 'RPL_INVITING',
414             '342' => 'RPL_SUMMONING',
415             '346' => 'RPL_INVITELIST',
416             '347' => 'RPL_ENDOFINVITELIST',
417             '348' => 'RPL_EXCEPTLIST',
418             '349' => 'RPL_ENDOFEXCEPTLIST',
419             '351' => 'RPL_VERSION',
420             '352' => 'RPL_WHOREPLY',
421             '353' => 'RPL_NAMREPLY',
422             '364' => 'RPL_LINKS',
423             '365' => 'RPL_ENDOFLINKS',
424             '366' => 'RPL_ENDOFNAMES',
425             '367' => 'RPL_BANLIST',
426             '368' => 'RPL_ENDOFBANLIST',
427             '369' => 'RPL_ENDOFWHOWAS',
428             '371' => 'RPL_INFO',
429             '372' => 'RPL_MOTD',
430             '374' => 'RPL_ENDOFINFO',
431             '375' => 'RPL_MOTDSTART',
432             '376' => 'RPL_ENDOFMOTD',
433             '381' => 'RPL_YOUREOPER',
434             '382' => 'RPL_REHASHING',
435             '383' => 'RPL_YOURESERVICE',
436             '384' => 'RPL_MYPORTIS',
437             '391' => 'RPL_TIME',
438             '392' => 'RPL_USERSSTART',
439             '393' => 'RPL_USERS',
440             '394' => 'RPL_ENDOFUSERS',
441             '395' => 'RPL_NOUSERS',
442             '401' => 'ERR_NOSUCHNICK',
443             '402' => 'ERR_NOSUCHSERVER',
444             '403' => 'ERR_NOSUCHCHANNEL',
445             '404' => 'ERR_CANNOTSENDTOCHAN',
446             '405' => 'ERR_TOOMANYCHANNELS',
447             '406' => 'ERR_WASNOSUCHNICK',
448             '407' => 'ERR_TOOMANYTARGETS',
449             '408' => 'ERR_NOSUCHSERVICE',
450             '409' => 'ERR_NOORIGIN',
451             '411' => 'ERR_NORECIPIENT',
452             '412' => 'ERR_NOTEXTTOSEND',
453             '413' => 'ERR_NOTOPLEVEL',
454             '414' => 'ERR_WILDTOPLEVEL',
455             '415' => 'ERR_BADMASK',
456             '421' => 'ERR_UNKNOWNCOMMAND',
457             '422' => 'ERR_NOMOTD',
458             '423' => 'ERR_NOADMININFO',
459             '424' => 'ERR_FILEERROR',
460             '431' => 'ERR_NONICKNAMEGIVEN',
461             '432' => 'ERR_ERRONEUSNICKNAME',
462             '433' => 'ERR_NICKNAMEINUSE',
463             '436' => 'ERR_NICKCOLLISION',
464             '437' => 'ERR_UNAVAILRESOURCE',
465             '441' => 'ERR_USERNOTINCHANNEL',
466             '442' => 'ERR_NOTONCHANNEL',
467             '443' => 'ERR_USERONCHANNEL',
468             '444' => 'ERR_NOLOGIN',
469             '445' => 'ERR_SUMMONDISABLED',
470             '446' => 'ERR_USERSDISABLED',
471             '451' => 'ERR_NOTREGISTERED',
472             '461' => 'ERR_NEEDMOREPARAMS',
473             '462' => 'ERR_ALREADYREGISTRED',
474             '463' => 'ERR_NOPERMFORHOST',
475             '464' => 'ERR_PASSWDMISMATCH',
476             '465' => 'ERR_YOUREBANNEDCREEP',
477             '466' => 'ERR_YOUWILLBEBANNED',
478             '467' => 'ERR_KEYSET',
479             '471' => 'ERR_CHANNELISFULL',
480             '472' => 'ERR_UNKNOWNMODE',
481             '473' => 'ERR_INVITEONLYCHAN',
482             '474' => 'ERR_BANNEDFROMCHAN',
483             '475' => 'ERR_BADCHANNELKEY',
484             '476' => 'ERR_BADCHANMASK',
485             '477' => 'ERR_NOCHANMODES',
486             '478' => 'ERR_BANLISTFULL',
487             '481' => 'ERR_NOPRIVILEGES',
488             '482' => 'ERR_CHANOPRIVSNEEDED',
489             '483' => 'ERR_CANTKILLSERVER',
490             '484' => 'ERR_RESTRICTED',
491             '485' => 'ERR_UNIQOPPRIVSNEEDED',
492             '491' => 'ERR_NOOPERHOST',
493             '492' => 'ERR_NOSERVICEHOST',
494             '501' => 'ERR_UMODEUNKNOWNFLAG',
495             '502' => 'ERR_USERSDONTMATCH',
496             );
497              
498             sub rfc_code_to_name {
499 6     6 1 2890 my ($code) = @_;
500 6   33     47 return $RFC_NUMCODE_MAP{$code} || $code;
501             }
502              
503             =item my (@lines) = split_unicode_string ($encoding, $string, $maxlinebytes)
504              
505             This function splits up C<$string> into multiple C<@lines> which are
506             not longer than C<$maxlinebytes> bytes. Encoding can be given in C<$encoding>.
507             (eg. 'utf-8'). But the output will not be encoded.
508              
509             This function takes care that your characters are not garbled.
510              
511             =cut
512              
513             sub split_unicode_string {
514 0     0 1   my ($enc, $str, $maxlen) = @_;
515              
516 0 0         return $str unless length (encode ($enc, $str)) > $maxlen;
517              
518 0           my $cur_out = '';
519 0           my @lines;
520              
521 0           while (length ($str) > 0) {
522              
523 0   0       while (length (encode ($enc, $cur_out)) <= $maxlen
524             && length ($str) > 0) {
525              
526 0           $cur_out .= substr $str, 0, 1, '';
527             }
528              
529 0           push @lines, $cur_out;
530 0           $cur_out = '';
531             }
532              
533             @lines
534 0           }
535              
536             =back
537              
538             =head1 AUTHOR
539              
540             Robin Redeker, C<< <elmex@ta-sa.org> >>
541              
542             =head1 SEE ALSO
543              
544             Internet Relay Chat Client To Client Protocol from February 2, 1997
545             http://www.invlogic.com/irc/ctcp.html
546              
547             RFC 1459 - Internet Relay Chat: Client Protocol
548              
549             =head1 COPYRIGHT & LICENSE
550              
551             Copyright 2006-2009 Robin Redeker, all rights reserved.
552              
553             This program is free software; you can redistribute it and/or modify it
554             under the same terms as Perl itself.
555              
556             =cut
557              
558             1;