File Coverage

blib/lib/IRC/Utils.pm
Criterion Covered Total %
statement 174 184 94.5
branch 81 106 76.4
condition 35 62 56.4
subroutine 28 28 100.0
pod 19 19 100.0
total 337 399 84.4


line stmt bran cond sub pod time code
1             package IRC::Utils;
2             BEGIN {
3 2     2   29196 $IRC::Utils::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 2     2   39 $IRC::Utils::VERSION = '0.12';
7             }
8              
9 2     2   17 use strict;
  2         4  
  2         64  
10 2     2   9 use warnings FATAL => 'all';
  2         13  
  2         101  
11              
12 2     2   889 use Encode qw(decode);
  2         11268  
  2         127  
13 2     2   12945 use Encode::Guess;
  2         14948  
  2         16  
14              
15             require Exporter;
16 2     2   154 use base qw(Exporter);
  2         5  
  2         781  
17             our @EXPORT_OK = qw(
18             uc_irc lc_irc parse_mode_line normalize_mask matches_mask matches_mask_array
19             unparse_mode_line gen_mode_change parse_user is_valid_nick_name eq_irc
20             decode_irc is_valid_chan_name has_color has_formatting strip_color
21             strip_formatting NORMAL BOLD UNDERLINE REVERSE ITALIC FIXED WHITE BLACK
22             BLUE GREEN RED BROWN PURPLE ORANGE YELLOW LIGHT_GREEN TEAL LIGHT_CYAN
23             LIGHT_BLUE PINK GREY LIGHT_GREY numeric_to_name name_to_numeric
24             );
25             our %EXPORT_TAGS = ( ALL => [@EXPORT_OK] );
26              
27             use constant {
28             # cancel all formatting and colors
29 2         8868 NORMAL => "\x0f",
30              
31             # formatting
32             BOLD => "\x02",
33             UNDERLINE => "\x1f",
34             REVERSE => "\x16",
35             ITALIC => "\x1d",
36             FIXED => "\x11",
37             BLINK => "\x06",
38              
39             # mIRC colors
40             WHITE => "\x0300",
41             BLACK => "\x0301",
42             BLUE => "\x0302",
43             GREEN => "\x0303",
44             RED => "\x0304",
45             BROWN => "\x0305",
46             PURPLE => "\x0306",
47             ORANGE => "\x0307",
48             YELLOW => "\x0308",
49             LIGHT_GREEN => "\x0309",
50             TEAL => "\x0310",
51             LIGHT_CYAN => "\x0311",
52             LIGHT_BLUE => "\x0312",
53             PINK => "\x0313",
54             GREY => "\x0314",
55             LIGHT_GREY => "\x0315",
56 2     2   13 };
  2         4  
57              
58             # list originally snatched from AnyEvent::IRC::Util
59             our %NUMERIC2NAME = (
60             '001' => 'RPL_WELCOME', # RFC2812
61             '002' => 'RPL_YOURHOST', # RFC2812
62             '003' => 'RPL_CREATED', # RFC2812
63             '004' => 'RPL_MYINFO', # RFC2812
64             '005' => 'RPL_ISUPPORT', # draft-brocklesby-irc-isupport-03
65             '008' => 'RPL_SNOMASK', # Undernet
66             '009' => 'RPL_STATMEMTOT', # Undernet
67             '010' => 'RPL_STATMEM', # Undernet
68             '020' => 'RPL_CONNECTING', # IRCnet
69             '014' => 'RPL_YOURCOOKIE', # IRCnet
70             '042' => 'RPL_YOURID', # IRCnet
71             '043' => 'RPL_SAVENICK', # IRCnet
72             '050' => 'RPL_ATTEMPTINGJUNC', # aircd
73             '051' => 'RPL_ATTEMPTINGREROUTE', # aircd
74             '200' => 'RPL_TRACELINK', # RFC1459
75             '201' => 'RPL_TRACECONNECTING', # RFC1459
76             '202' => 'RPL_TRACEHANDSHAKE', # RFC1459
77             '203' => 'RPL_TRACEUNKNOWN', # RFC1459
78             '204' => 'RPL_TRACEOPERATOR', # RFC1459
79             '205' => 'RPL_TRACEUSER', # RFC1459
80             '206' => 'RPL_TRACESERVER', # RFC1459
81             '207' => 'RPL_TRACESERVICE', # RFC2812
82             '208' => 'RPL_TRACENEWTYPE', # RFC1459
83             '209' => 'RPL_TRACECLASS', # RFC2812
84             '210' => 'RPL_STATS', # aircd
85             '211' => 'RPL_STATSLINKINFO', # RFC1459
86             '212' => 'RPL_STATSCOMMANDS', # RFC1459
87             '213' => 'RPL_STATSCLINE', # RFC1459
88             '214' => 'RPL_STATSNLINE', # RFC1459
89             '215' => 'RPL_STATSILINE', # RFC1459
90             '216' => 'RPL_STATSKLINE', # RFC1459
91             '217' => 'RPL_STATSQLINE', # RFC1459
92             '218' => 'RPL_STATSYLINE', # RFC1459
93             '219' => 'RPL_ENDOFSTATS', # RFC1459
94             '221' => 'RPL_UMODEIS', # RFC1459
95             '231' => 'RPL_SERVICEINFO', # RFC1459
96             '233' => 'RPL_SERVICE', # RFC1459
97             '234' => 'RPL_SERVLIST', # RFC1459
98             '235' => 'RPL_SERVLISTEND', # RFC1459
99             '239' => 'RPL_STATSIAUTH', # IRCnet
100             '241' => 'RPL_STATSLLINE', # RFC1459
101             '242' => 'RPL_STATSUPTIME', # RFC1459
102             '243' => 'RPL_STATSOLINE', # RFC1459
103             '244' => 'RPL_STATSHLINE', # RFC1459
104             '245' => 'RPL_STATSSLINE', # Bahamut, IRCnet, Hybrid
105             '250' => 'RPL_STATSCONN', # ircu, Unreal
106             '251' => 'RPL_LUSERCLIENT', # RFC1459
107             '252' => 'RPL_LUSEROP', # RFC1459
108             '253' => 'RPL_LUSERUNKNOWN', # RFC1459
109             '254' => 'RPL_LUSERCHANNELS', # RFC1459
110             '255' => 'RPL_LUSERME', # RFC1459
111             '256' => 'RPL_ADMINME', # RFC1459
112             '257' => 'RPL_ADMINLOC1', # RFC1459
113             '258' => 'RPL_ADMINLOC2', # RFC1459
114             '259' => 'RPL_ADMINEMAIL', # RFC1459
115             '261' => 'RPL_TRACELOG', # RFC1459
116             '262' => 'RPL_TRACEEND', # RFC2812
117             '263' => 'RPL_TRYAGAIN', # RFC2812
118             '265' => 'RPL_LOCALUSERS', # aircd, Bahamut, Hybrid
119             '266' => 'RPL_GLOBALUSERS', # aircd, Bahamut, Hybrid
120             '267' => 'RPL_START_NETSTAT', # aircd
121             '268' => 'RPL_NETSTAT', # aircd
122             '269' => 'RPL_END_NETSTAT', # aircd
123             '270' => 'RPL_PRIVS', # ircu
124             '271' => 'RPL_SILELIST', # ircu
125             '272' => 'RPL_ENDOFSILELIST', # ircu
126             '300' => 'RPL_NONE', # RFC1459
127             '301' => 'RPL_AWAY', # RFC1459
128             '302' => 'RPL_USERHOST', # RFC1459
129             '303' => 'RPL_ISON', # RFC1459
130             '305' => 'RPL_UNAWAY', # RFC1459
131             '306' => 'RPL_NOWAWAY', # RFC1459
132             '307' => 'RPL_WHOISREGNICK', # Bahamut, Unreal, Plexus
133             '310' => 'RPL_WHOISMODES', # Plexus
134             '311' => 'RPL_WHOISUSER', # RFC1459
135             '312' => 'RPL_WHOISSERVER', # RFC1459
136             '313' => 'RPL_WHOISOPERATOR', # RFC1459
137             '314' => 'RPL_WHOWASUSER', # RFC1459
138             '315' => 'RPL_ENDOFWHO', # RFC1459
139             '317' => 'RPL_WHOISIDLE', # RFC1459
140             '318' => 'RPL_ENDOFWHOIS', # RFC1459
141             '319' => 'RPL_WHOISCHANNELS', # RFC1459
142             '321' => 'RPL_LISTSTART', # RFC1459
143             '322' => 'RPL_LIST', # RFC1459
144             '323' => 'RPL_LISTEND', # RFC1459
145             '324' => 'RPL_CHANNELMODEIS', # RFC1459
146             '325' => 'RPL_UNIQOPIS', # RFC2812
147             '328' => 'RPL_CHANNEL_URL', # Bahamut, AustHex
148             '329' => 'RPL_CREATIONTIME', # Bahamut
149             '330' => 'RPL_WHOISACCOUNT', # ircu
150             '331' => 'RPL_NOTOPIC', # RFC1459
151             '332' => 'RPL_TOPIC', # RFC1459
152             '333' => 'RPL_TOPICWHOTIME', # ircu
153             '338' => 'RPL_WHOISACTUALLY', # Bahamut, ircu
154             '340' => 'RPL_USERIP', # ircu
155             '341' => 'RPL_INVITING', # RFC1459
156             '342' => 'RPL_SUMMONING', # RFC1459
157             '345' => 'RPL_INVITED', # GameSurge
158             '346' => 'RPL_INVITELIST', # RFC2812
159             '347' => 'RPL_ENDOFINVITELIST', # RFC2812
160             '348' => 'RPL_EXCEPTLIST', # RFC2812
161             '349' => 'RPL_ENDOFEXCEPTLIST', # RFC2812
162             '351' => 'RPL_VERSION', # RFC1459
163             '352' => 'RPL_WHOREPLY', # RFC1459
164             '353' => 'RPL_NAMREPLY', # RFC1459
165             '354' => 'RPL_WHOSPCRPL', # ircu
166             '355' => 'RPL_NAMREPLY_', # QuakeNet
167             '361' => 'RPL_KILLDONE', # RFC1459
168             '362' => 'RPL_CLOSING', # RFC1459
169             '363' => 'RPL_CLOSEEND', # RFC1459
170             '364' => 'RPL_LINKS', # RFC1459
171             '365' => 'RPL_ENDOFLINKS', # RFC1459
172             '366' => 'RPL_ENDOFNAMES', # RFC1459
173             '367' => 'RPL_BANLIST', # RFC1459
174             '368' => 'RPL_ENDOFBANLIST', # RFC1459
175             '369' => 'RPL_ENDOFWHOWAS', # RFC1459
176             '371' => 'RPL_INFO', # RFC1459
177             '372' => 'RPL_MOTD', # RFC1459
178             '373' => 'RPL_INFOSTART', # RFC1459
179             '374' => 'RPL_ENDOFINFO', # RFC1459
180             '375' => 'RPL_MOTDSTART', # RFC1459
181             '376' => 'RPL_ENDOFMOTD', # RFC1459
182             '381' => 'RPL_YOUREOPER', # RFC1459
183             '382' => 'RPL_REHASHING', # RFC1459
184             '383' => 'RPL_YOURESERVICE', # RFC2812
185             '384' => 'RPL_MYPORTIS', # RFC1459
186             '385' => 'RPL_NOTOPERANYMORE', # AustHex, Hybrid, Unreal
187             '391' => 'RPL_TIME', # RFC1459
188             '392' => 'RPL_USERSSTART', # RFC1459
189             '393' => 'RPL_USERS', # RFC1459
190             '394' => 'RPL_ENDOFUSERS', # RFC1459
191             '395' => 'RPL_NOUSERS', # RFC1459
192             '396' => 'RPL_HOSTHIDDEN', # Undernet
193             '401' => 'ERR_NOSUCHNICK', # RFC1459
194             '402' => 'ERR_NOSUCHSERVER', # RFC1459
195             '403' => 'ERR_NOSUCHCHANNEL', # RFC1459
196             '404' => 'ERR_CANNOTSENDTOCHAN', # RFC1459
197             '405' => 'ERR_TOOMANYCHANNELS', # RFC1459
198             '406' => 'ERR_WASNOSUCHNICK', # RFC1459
199             '407' => 'ERR_TOOMANYTARGETS', # RFC1459
200             '408' => 'ERR_NOSUCHSERVICE', # RFC2812
201             '409' => 'ERR_NOORIGIN', # RFC1459
202             '411' => 'ERR_NORECIPIENT', # RFC1459
203             '412' => 'ERR_NOTEXTTOSEND', # RFC1459
204             '413' => 'ERR_NOTOPLEVEL', # RFC1459
205             '414' => 'ERR_WILDTOPLEVEL', # RFC1459
206             '415' => 'ERR_BADMASK', # RFC2812
207             '421' => 'ERR_UNKNOWNCOMMAND', # RFC1459
208             '422' => 'ERR_NOMOTD', # RFC1459
209             '423' => 'ERR_NOADMININFO', # RFC1459
210             '424' => 'ERR_FILEERROR', # RFC1459
211             '425' => 'ERR_NOOPERMOTD', # Unreal
212             '429' => 'ERR_TOOMANYAWAY', # Bahamut
213             '430' => 'ERR_EVENTNICKCHANGE', # AustHex
214             '431' => 'ERR_NONICKNAMEGIVEN', # RFC1459
215             '432' => 'ERR_ERRONEUSNICKNAME', # RFC1459
216             '433' => 'ERR_NICKNAMEINUSE', # RFC1459
217             '436' => 'ERR_NICKCOLLISION', # RFC1459
218             '439' => 'ERR_TARGETTOOFAST', # ircu
219             '440' => 'ERR_SERCVICESDOWN', # Bahamut, Unreal
220             '441' => 'ERR_USERNOTINCHANNEL', # RFC1459
221             '442' => 'ERR_NOTONCHANNEL', # RFC1459
222             '443' => 'ERR_USERONCHANNEL', # RFC1459
223             '444' => 'ERR_NOLOGIN', # RFC1459
224             '445' => 'ERR_SUMMONDISABLED', # RFC1459
225             '446' => 'ERR_USERSDISABLED', # RFC1459
226             '447' => 'ERR_NONICKCHANGE', # Unreal
227             '449' => 'ERR_NOTIMPLEMENTED', # Undernet
228             '451' => 'ERR_NOTREGISTERED', # RFC1459
229             '455' => 'ERR_HOSTILENAME', # Unreal
230             '459' => 'ERR_NOHIDING', # Unreal
231             '460' => 'ERR_NOTFORHALFOPS', # Unreal
232             '461' => 'ERR_NEEDMOREPARAMS', # RFC1459
233             '462' => 'ERR_ALREADYREGISTRED', # RFC1459
234             '463' => 'ERR_NOPERMFORHOST', # RFC1459
235             '464' => 'ERR_PASSWDMISMATCH', # RFC1459
236             '465' => 'ERR_YOUREBANNEDCREEP', # RFC1459
237             '466' => 'ERR_YOUWILLBEBANNED', # RFC1459
238             '467' => 'ERR_KEYSET', # RFC1459
239             '469' => 'ERR_LINKSET', # Unreal
240             '471' => 'ERR_CHANNELISFULL', # RFC1459
241             '472' => 'ERR_UNKNOWNMODE', # RFC1459
242             '473' => 'ERR_INVITEONLYCHAN', # RFC1459
243             '474' => 'ERR_BANNEDFROMCHAN', # RFC1459
244             '475' => 'ERR_BADCHANNELKEY', # RFC1459
245             '476' => 'ERR_BADCHANMASK', # RFC2812
246             '477' => 'ERR_NOCHANMODES', # RFC2812
247             '478' => 'ERR_BANLISTFULL', # RFC2812
248             '481' => 'ERR_NOPRIVILEGES', # RFC1459
249             '482' => 'ERR_CHANOPRIVSNEEDED', # RFC1459
250             '483' => 'ERR_CANTKILLSERVER', # RFC1459
251             '484' => 'ERR_RESTRICTED', # RFC2812
252             '485' => 'ERR_UNIQOPPRIVSNEEDED', # RFC2812
253             '488' => 'ERR_TSLESSCHAN', # IRCnet
254             '491' => 'ERR_NOOPERHOST', # RFC1459
255             '492' => 'ERR_NOSERVICEHOST', # RFC1459
256             '493' => 'ERR_NOFEATURE', # ircu
257             '494' => 'ERR_BADFEATURE', # ircu
258             '495' => 'ERR_BADLOGTYPE', # ircu
259             '496' => 'ERR_BADLOGSYS', # ircu
260             '497' => 'ERR_BADLOGVALUE', # ircu
261             '498' => 'ERR_ISOPERLCHAN', # ircu
262             '501' => 'ERR_UMODEUNKNOWNFLAG', # RFC1459
263             '502' => 'ERR_USERSDONTMATCH', # RFC1459
264             '503' => 'ERR_GHOSTEDCLIENT', # Hybrid
265             );
266              
267             our %NAME2NUMERIC;
268             while (my ($key, $val) = each %NUMERIC2NAME) {
269             $NAME2NUMERIC{$val} = $key;
270             }
271              
272             sub numeric_to_name {
273 1     1 1 2315 my ($code) = @_;
274 1         13 return $NUMERIC2NAME{$code};
275             }
276              
277             sub name_to_numeric {
278 1     1 1 3 my ($name) = @_;
279 1         7 return $NAME2NUMERIC{$name};
280             }
281              
282             sub uc_irc {
283 12     12 1 31 my ($value, $type) = @_;
284 12 50       24 return if !defined $value;
285 12 100       29 $type = 'rfc1459' if !defined $type;
286 12         22 $type = lc $type;
287              
288 12 100       36 if ($type eq 'ascii') {
    100          
289 1         3 $value =~ tr/a-z/A-Z/;
290             }
291             elsif ($type eq 'strict-rfc1459') {
292 1         3 $value =~ tr/a-z{}|/A-Z[]\\/;
293             }
294             else {
295 10         18 $value =~ tr/a-z{}|^/A-Z[]\\~/;
296             }
297              
298 12         47 return $value;
299             }
300              
301             sub lc_irc {
302 6     6 1 14 my ($value, $type) = @_;
303 6 50       17 return if !defined $value;
304 6 100       15 $type = 'rfc1459' if !defined $type;
305 6         11 $type = lc $type;
306              
307 6 100       21 if ($type eq 'ascii') {
    100          
308 1         5 $value =~ tr/A-Z/a-z/;
309             }
310             elsif ($type eq 'strict-rfc1459') {
311 1         3 $value =~ tr/A-Z[]\\/a-z{}|/;
312             }
313             else {
314 4         8 $value =~ tr/A-Z[]\\~/a-z{}|^/;
315             }
316              
317 6         31 return $value;
318             }
319              
320             sub eq_irc {
321 1     1 1 3 my ($first, $second, $type) = @_;
322 1 50 33     8 return if !defined $first || !defined $second;
323 1 50       3 return 1 if lc_irc($first, $type) eq lc_irc($second, $type);
324 0         0 return;
325             }
326              
327             sub parse_mode_line {
328 3     3 1 2250 my @args = @_;
329              
330 3         10 my $chanmodes = [qw(beI k l imnpstaqr)];
331 3         6 my $statmodes = 'ohv';
332 3         7 my $hashref = { };
333 3         5 my $count = 0;
334              
335 3         12 while (my $arg = shift @args) {
336 3 50 66     35 if ( ref $arg eq 'ARRAY' ) {
    50          
    50          
337 0         0 $chanmodes = $arg;
338 0         0 next;
339             }
340             elsif (ref $arg eq 'HASH') {
341 0         0 $statmodes = join '', keys %{ $arg };
  0         0  
342 0         0 next;
343             }
344             elsif ($arg =~ /^[-+]/ or $count == 0) {
345 3         5 my $action = '+';
346 3         11 for my $char (split //, $arg) {
347 6 100 100     27 if ($char eq '+' or $char eq '-') {
348 2         4 $action = $char;
349             }
350             else {
351 4         6 push @{ $hashref->{modes} }, $action . $char;
  4         16  
352             }
353              
354 6 100 33     97 if (length $chanmodes->[0] && length $chanmodes->[1] && length $statmodes
      33        
      66        
355             && $char =~ /[$statmodes$chanmodes->[0]$chanmodes->[1]]/) {
356 4         14 push @{ $hashref->{args} }, shift @args;
  4         13  
357             }
358              
359 6 50 66     74 if (length $chanmodes->[2] && $action eq '+' && $char =~ /[$chanmodes->[2]]/) {
      66        
360 0         0 push @{ $hashref->{args} }, shift @args;
  0         0  
361             }
362             }
363             }
364             else {
365 0         0 push @{ $hashref->{args} }, $arg;
  0         0  
366             }
367 3         11 $count++;
368             }
369              
370 3         12 return $hashref;
371             }
372              
373             sub normalize_mask {
374 2     2 1 1247 my ($arg) = @_;
375 2 50       6 return if !defined $arg;
376              
377 2         5 $arg =~ s/\*{2,}/*/g;
378 2         3 my @mask;
379             my $remainder;
380 2 100 66     13 if ($arg !~ /!/ and $arg =~ /@/) {
381 1         2 $remainder = $arg;
382 1         2 $mask[0] = '*';
383             }
384             else {
385 1         5 ($mask[0], $remainder) = split /!/, $arg, 2;
386             }
387              
388 2 100       8 $remainder =~ s/!//g if defined $remainder;
389 2 100       7 @mask[1..2] = split(/@/, $remainder, 2) if defined $remainder;
390 2 100       7 $mask[2] =~ s/@//g if defined $mask[2];
391              
392 2         5 for my $i (1..2) {
393 4 100       13 $mask[$i] = '*' if !defined $mask[$i];
394             }
395 2         9 return $mask[0] . '!' . $mask[1] . '@' . $mask[2];
396             }
397              
398             sub unparse_mode_line {
399 4     4 1 8 my ($line) = @_;
400 4 50 33     26 return if !defined $line || !length $line;
401              
402 4         8 my $action; my $return;
403 4         17 for my $mode ( split(//,$line) ) {
404 48 100 100     310 if ($mode =~ /^(\+|-)$/ && (!$action || $mode ne $action)) {
      66        
405 9         12 $return .= $mode;
406 9         12 $action = $mode;
407 9         13 next;
408             }
409 39 100 100     155 $return .= $mode if ($mode ne '+' and $mode ne '-');
410             }
411 4         18 $return =~ s/[+-]$//;
412 4         32 return $return;
413             }
414              
415             sub gen_mode_change {
416 3     3 1 9 my ($before, $after) = @_;
417 3 50       10 $before = '' if !defined $before;
418 3 50       8 $after = '' if !defined $after;
419              
420 3         12 my @before = split //, $before;
421 3         11 my @after = split //, $after;
422 3         6 my $string = '';
423 3         9 my @hunks = _diff(\@before, \@after);
424 3         20 $string .= $_->[0] . $_->[1] for @hunks;
425              
426 3         8 return unparse_mode_line($string);
427             }
428              
429             sub is_valid_nick_name {
430 2     2 1 25 my ($nickname) = @_;
431 2 50 33     12 return if !defined $nickname || !length $nickname;
432 2 100       14 return 1 if $nickname =~ /^[A-Za-z_`\-^\|\\\{}\[\]][A-Za-z_0-9`\-^\|\\\{}\[\]]*$/;
433 1         4 return;
434             }
435              
436             sub is_valid_chan_name {
437 3     3 1 7 my $channel = shift;
438 3   50     21 my $chantypes = shift || ['#', '&'];
439 3 50       10 return if !@$chantypes;
440 3         7 my $chanprefix = join '', @$chantypes;
441 3 50 33     25 return if !defined $channel || !length $channel;
442              
443 3 100       14 return if bytes::length($channel) > 200;
444 2 100       7164 return 1 if $channel =~ /^[$chanprefix][^ \a\0\012\015,:]+$/;
445 1         5 return;
446             }
447              
448             sub matches_mask_array {
449 2     2 1 4 my ($masks, $matches, $mapping) = @_;
450              
451 2 50 33     9 return if !defined $masks || !defined $matches;
452 2 50       7 return if ref $masks ne 'ARRAY';
453 2 50       4 return if ref $matches ne 'ARRAY';
454 2         3 my $ref = { };
455              
456 2         4 for my $mask (@$masks) {
457 2         3 for my $match (@$matches) {
458 2 100       3 if (matches_mask($mask, $match, $mapping)) {
459 1         3 push @{ $ref->{ $mask } }, $match;
  1         12  
460             }
461             }
462             }
463              
464 2         10 return $ref;
465             }
466              
467             sub matches_mask {
468 4     4 1 404 my ($mask, $match, $mapping) = @_;
469 4 50 33     15 return if !defined $mask || !length $mask;
470 4 50 33     15 return if !defined $match || !length $match;
471              
472 4         8 my $umask = quotemeta uc_irc($mask, $mapping);
473 4         14 $umask =~ s/\\\*/[\x01-\xFF]{0,}/g;
474 4         6 $umask =~ s/\\\?/[\x01-\xFF]{1,1}/g;
475 4         8 $match = uc_irc($match, $mapping);
476              
477 4 100       61 return 1 if $match =~ /^$umask$/;
478 2         8 return;
479             }
480              
481             sub parse_user {
482 2     2 1 12 my ($user) = @_;
483 2 50       6 return if !defined $user;
484              
485 2         11 my ($n, $u, $h) = split /[!@]/, $user;
486 2 100       8 return ($n, $u, $h) if wantarray();
487 1         4 return $n;
488             }
489              
490             sub has_color {
491 3     3 1 1713 my ($string) = @_;
492 3 50       10 return if !defined $string;
493 3 100       18 return 1 if $string =~ /[\x03\x04\x1B]/;
494 1         7 return;
495             }
496              
497             sub has_formatting {
498 5     5 1 7 my ($string) = @_;
499 5 50       13 return if !defined $string;
500 5 100       24 return 1 if $string =~/[\x02\x1f\x16\x1d\x11\x06]/;
501 3         10 return;
502             }
503              
504             sub strip_color {
505 4     4 1 643 my ($string) = @_;
506 4 50       13 return if !defined $string;
507              
508             # mIRC colors
509 4         32 $string =~ s/\x03(?:,\d{1,2}|\d{1,2}(?:,\d{1,2})?)?//g;
510              
511             # RGB colors supported by some clients
512 4         49 $string =~ s/\x04[0-9a-fA-F]{0,6}//ig;
513              
514             # see ECMA-48 + advice by urxvt author
515 4         8 $string =~ s/\x1B\[.*?[\x00-\x1F\x40-\x7E]//g;
516              
517             # strip cancellation codes too if there are no formatting codes
518 4 100       11 $string =~ s/\x0f//g if !has_formatting($string);
519 4         16 return $string;
520             }
521              
522             sub strip_formatting {
523 2     2 1 7 my ($string) = @_;
524 2 50       8 return if !defined $string;
525 2         18 $string =~ s/[\x02\x1f\x16\x1d\x11\x06]//g;
526              
527             # strip cancellation codes too if there are no color codes
528 2 100       8 $string =~ s/\x0f//g if !has_color($string);
529              
530 2         8 return $string;
531             }
532              
533             sub decode_irc {
534 2     2 1 29848 my ($line) = @_;
535 2         18 my $utf8 = guess_encoding($line, 'utf8');
536 2 100       532 return ref $utf8 ? decode('utf8', $line) : decode('cp1252', $line);
537             }
538              
539             sub _diff {
540 3     3   6 my ($before, $after) = @_;
541 3         6 my %in_before;
542 3         12 @in_before{@$before} = ();
543 3         11 my %in_after;
544 3         10 @in_after{@$after} = ();
545 3         4 my (@diff, %seen);
546              
547 3         5 for my $seen (@$before) {
548 8 100 66     40 next if exists $seen{$seen} || exists $in_after{$seen};
549 6         8 $seen{$seen} = 1;
550 6         17 push @diff, ['-', $seen];
551             }
552              
553 3         7 %seen = ();
554              
555 3         6 for my $seen (@$after) {
556 12 100 66     59 next if exists $seen{$seen} || exists $in_before{$seen};
557 10         15 $seen{$seen} = 1;
558 10         28 push @diff, ['+', $seen];
559             }
560              
561 3         18 return @diff;
562             }
563              
564             1;
565              
566             =encoding utf8
567              
568             =head1 NAME
569              
570             IRC::Utils - Common utilities for IRC-related tasks
571              
572             =head1 SYNOPSIS
573              
574             use strict;
575             use warnings;
576              
577             use IRC::Utils ':ALL';
578              
579             my $nickname = '^Lame|BOT[moo]';
580             my $uppercase_nick = uc_irc($nickname);
581             my $lowercase_nick = lc_irc($nickname);
582              
583             print "They're equivalent\n" if eq_irc($uppercase_nick, $lowercase_nick);
584              
585             my $mode_line = 'ov+b-i Bob sue stalin*!*@*';
586             my $hashref = parse_mode_line($mode_line);
587              
588             my $banmask = 'stalin*';
589             my $full_banmask = normalize_mask($banmask);
590              
591             if (matches_mask($full_banmask, 'stalin!joe@kremlin.ru')) {
592             print "EEK!";
593             }
594              
595             my $decoded = irc_decode($raw_irc_message);
596             print $decoded, "\n";
597              
598             if (has_color($message)) {
599             print 'COLOR CODE ALERT!\n";
600             }
601              
602             my $results_hashref = matches_mask_array(\@masks, \@items_to_match_against);
603              
604             my $nick = parse_user('stalin!joe@kremlin.ru');
605             my ($nick, $user, $host) = parse_user('stalin!joe@kremlin.ru');
606              
607             =head1 DESCRIPTION
608              
609             The functions in this module take care of many of the tasks you are faced
610             with when working with IRC. Mode lines, ban masks, message encoding and
611             formatting, etc.
612              
613             =head1 FUNCTIONS
614              
615             =head2 C
616              
617             Takes one mandatory parameter, a string to convert to IRC uppercase, and one
618             optional parameter, the casemapping of the ircd (which can be B<'rfc1459'>,
619             B<'strict-rfc1459'> or B<'ascii'>. Default is B<'rfc1459'>). Returns the IRC
620             uppercase equivalent of the passed string.
621              
622             =head2 C
623              
624             Takes one mandatory parameter, a string to convert to IRC lowercase, and one
625             optional parameter, the casemapping of the ircd (which can be B<'rfc1459'>,
626             B<'strict-rfc1459'> or B<'ascii'>. Default is B<'rfc1459'>). Returns the IRC
627             lowercase equivalent of the passed string.
628              
629             =head2 C
630              
631             Takes two mandatory parameters, IRC strings (channels or nicknames) to
632             compare. A third, optional parameter specifies the casemapping. Returns true
633             if the two strings are equivalent, false otherwise
634              
635             # long version
636             lc_irc($one, $map) eq lc_irc($two, $map)
637              
638             # short version
639             eq_irc($one, $two, $map)
640              
641             =head2 C
642              
643             Takes a list representing an IRC mode line. Returns a hashref. Optionally
644             you can also supply an arrayref and a hashref to specify valid channel
645             modes (default: C<[qw(beI k l imnpstaqr)]>) and status modes (default:
646             C<< {o => '@', h => '%', v => '+'} >>), respectively.
647              
648             If the modeline
649             couldn't be parsed the hashref will be empty. On success the following keys
650             will be available in the hashref:
651              
652             B<'modes'>, an arrayref of normalised modes;
653              
654             B<'args'>, an arrayref of applicable arguments to the modes;
655              
656             Example:
657              
658             my $hashref = parse_mode_line( 'ov+b-i', 'Bob', 'sue', 'stalin*!*@*' );
659              
660             # $hashref will be:
661             {
662             modes => [ '+o', '+v', '+b', '-i' ],
663             args => [ 'Bob', 'sue', 'stalin*!*@*' ],
664             }
665              
666             =head2 C
667              
668             Takes one parameter, a string representing an IRC mask. Returns a normalised
669             full mask.
670              
671             Example:
672              
673             $fullbanmask = normalize_mask( 'stalin*' );
674              
675             # $fullbanmask will be: 'stalin*!*@*';
676              
677             =head2 C
678              
679             Takes two parameters, a string representing an IRC mask and something to
680             match against the IRC mask, such as a nick!user@hostname string. Returns
681             a true value if they match, a false value otherwise. Optionally, one may
682             pass the casemapping (see L|/uc_irc>), as this function uses
683             C internally.
684              
685             =head2 C
686              
687             Takes two array references, the first being a list of strings representing
688             IRC masks, the second a list of somethings to test against the masks. Returns
689             an empty hashref if there are no matches. Otherwise, the keys will be the
690             masks matched, each value being an arrayref of the strings that matched it.
691             Optionally, one may pass the casemapping (see L|/uc_irc>), as
692             this function uses C internally.
693              
694             =head2 C
695              
696             Takes one argument, a string representing a number of mode changes. Returns
697             a condensed version of the changes.
698              
699             my $mode_line = unparse_mode_line('+o+o+o-v+v');
700             $mode_line is now '+ooo-v+v'
701              
702             =head2 C
703              
704             Takes two arguments, strings representing a set of IRC user modes before and
705             after a change. Returns a string representing what changed.
706              
707             my $mode_change = gen_mode_change('abcde', 'befmZ');
708             $mode_change is now '-acd+fmZ'
709              
710             =head2 C
711              
712             Takes one parameter, a string representing a user in the form
713             nick!user@hostname. In a scalar context it returns just the nickname.
714             In a list context it returns a list consisting of the nick, user and hostname,
715             respectively.
716              
717             =head2 C
718              
719             Takes one argument, a channel name to validate. Returns true or false if the
720             channel name is valid or not. You can supply a second argument, an array of
721             characters of allowed channel prefixes. Defaults to C<['#', '&']>.
722              
723             =head2 C
724              
725             Takes one argument, a nickname to validate. Returns true or false if the
726             nickname is valid or not.
727              
728             =head2 C
729              
730             Takes an IRC server numerical reply code (e.g. '001') as an argument, and
731             returns the corresponding name (e.g. 'RPL_WELCOME').
732              
733             =head2 C
734              
735             Takes an IRC server reply name (e.g. 'RPL_WELCOME') as an argument, and returns the
736             corresponding numerical code (e.g. '001').
737              
738             =head2 C
739              
740             Takes one parameter, a string of IRC text. Returns true if it contains any IRC
741             color codes, false otherwise. Useful if you want your bot to kick users for
742             (ab)using colors. :)
743              
744             =head2 C
745              
746             Takes one parameter, a string of IRC text. Returns true if it contains any IRC
747             formatting codes, false otherwise.
748              
749             =head2 C
750              
751             Takes one parameter, a string of IRC text. Returns the string stripped of all
752             IRC color codes.
753              
754             =head2 C
755              
756             Takes one parameter, a string of IRC text. Returns the string stripped of all
757             IRC formatting codes.
758              
759             =head2 C
760              
761             This function takes a byte string (i.e. an unmodified IRC message) and
762             returns a text string. Since the source encoding might have been UTF-8,
763             you should store it with UTF-8 or some other Unicode encoding in your
764             file/database/whatever to be safe. For a more detailed discussion, see
765             L.
766              
767             use IRC::Utils qw(decode_irc);
768              
769             sub message_handler {
770             my ($nick, $channel, $message) = @_;
771              
772             # not wise, $message is a byte string of unkown encoding
773             print $message, "\n";
774              
775             $message = decode_irc($what);
776              
777             # good, $message is a text string
778             print $message, "\n";
779             }
780              
781             =head1 CONSTANTS
782              
783             Use the following constants to add formatting and mIRC color codes to IRC
784             messages.
785              
786             Normal text:
787              
788             NORMAL
789              
790             Formatting:
791              
792             BOLD
793             UNDERLINE
794             REVERSE
795             ITALIC
796             FIXED
797              
798             Colors:
799              
800             WHITE
801             BLACK
802             BLUE
803             GREEN
804             RED
805             BROWN
806             PURPLE
807             ORANGE
808             YELLOW
809             LIGHT_GREEN
810             TEAL
811             LIGHT_CYAN
812             LIGHT_BLUE
813             PINK
814             GREY
815             LIGHT_GREY
816              
817             Individual non-color formatting codes can be cancelled with their
818             corresponding constant, but you can also cancel all of them at once with
819             C. To cancel the effect of color codes, you must use C.
820             which of course has the side effect of cancelling all other formatting codes
821             as well.
822              
823             $msg = 'This word is '.YELLOW.'yellow'.NORMAL.' while this word is'.BOLD.'bold'.BOLD;
824             $msg = UNDERLINE.BOLD.'This sentence is both underlined and bold.'.NORMAL;
825              
826             =head1 ENCODING
827              
828             =head2 Messages
829              
830             The only encoding requirement the IRC protocol places on its messages is
831             that they be 8-bits and ASCII-compatible. This has resulted in most of the
832             Western world settling on ASCII-compatible Latin-1 (usually Microsoft's
833             CP1252, a Latin-1 variant) as a convention. Recently, popular IRC clients
834             (mIRC, xchat, certain irssi configurations) have begun sending a mixture of
835             CP1252 and UTF-8 over the wire to allow more characters without breaking
836             backward compatibility (too much). They send CP1252 encoded messages if the
837             characters fit within that encoding, otherwise falling back to UTF-8, and
838             likewise autodetecting the encoding (UTF-8 or CP1252) of incoming messages.
839             Since writing text with mixed encoding to a file, terminal, or database is
840             not a good idea, you need a way to decode messages from IRC.
841             L|/decode_irc> will do that.
842              
843             =head2 Channel names
844              
845             The matter is complicated further by the fact that some servers allow
846             non-ASCII characters in channel names. IRC modules generally don't
847             explicitly encode or decode any IRC traffic, but they do have to
848             concatenate parts of a message (e.g. a channel name and a message) before
849             sending it over the wire. So when you do something like
850             C<< privmsg($channel, 'æði') >>, where C<$channel> is the unmodified
851             channel name (a byte string) you got from an earlier IRC message, the
852             channel name will get double-encoded when concatenated with your message (a
853             non-ASCII text string) if the channel name contains non-ASCII bytes.
854              
855             To prevent this, you can't simply L the channel name and
856             then use it. C<'#æði'> in CP1252 is not the same channel as C<'#æði'> in
857             UTF-8, since they are encoded as different sequences of bytes, and the IRC
858             server only cares about the byte representation. Therefore, when using a
859             channel name you got from the server (e.g. when replying to message), you
860             should use the original byte string (before it has been decoded with
861             L|/decode_irc>), and encode any other parameters (with
862             L|Encode>) so that your message will be concatenated
863             correctly. At some point, you'll probably want to print the channel name,
864             write it to a log file or use it in a filename, so you'll eventually have to
865             decode it, at which point the UTF-8 C<#æði> and CP1252 C<#æði> will have to
866             be considered equivalent.
867              
868             use Encode qw(encode_utf8 encode);
869              
870             sub message_handler {
871             # these three are all byte strings
872             my ($nick, $channel, $message) = @_;
873              
874             # bad: if $channel has any non-ASCII bytes, they will get double-encoded
875             privmsg($channel, 'æði');
876              
877             # bad: if $message has any non-ASCII bytes, they will get double-encoded
878             privmsg('#æði', $message);
879              
880             # good: both are byte strings already, so they will concatenate correctly
881             privmsg($channel, $message);
882              
883             # good: both are text strings (Latin1 as per Perl's default), so
884             # they'll be concatenated correctly
885             privmsg('#æði', 'æði');
886              
887             # good: similar to the last one, except now they're using UTF-8, which
888             # means that the channel is actually not the same as above
889             use utf8;
890             privmsg('#æði', 'æði');
891              
892             # good: $channel and $msg_bytes are both byte strings
893             my $msg_bytes = encode_utf8('æði');
894             privmsg($channel, $msg_bytes);
895              
896             # good: $chan_bytes and $message are both byte strings
897             # here we're sending a message to the utf8-encoded #æði
898             my $utf8_bytes = encode_utf8('#æði');
899             privmsg($utf8_bytes, $message);
900              
901             # good: $chan_bytes and $message are both byte strings
902             # here we're sending a message to the cp1252-encoded #æði
903             my $cp1252_bytes = encode('cp1252', '#æði');
904             privmsg($cp1252_bytes, $message);
905              
906             # bad: $channel is in an undetermined encoding
907             log_message("Got message from $channel");
908              
909             # good: using the decoded version of $channel
910             log_message("Got message from ".decode_irc($channel));
911             }
912              
913             See also L, L,
914             L, L, and
915             L.
916              
917             =head1 AUTHOR
918              
919             Hinrik Ern SigurEsson (C irc.perl.org, or C @ FreeNode).
920              
921             Chris C Williams
922              
923             =head1 SEE ALSO
924              
925             L
926              
927             L
928              
929             =cut