File Coverage

blib/lib/Protocol/IRC/Client.pm
Criterion Covered Total %
statement 157 160 98.1
branch 47 60 78.3
condition 8 9 88.8
subroutine 19 20 95.0
pod 2 14 14.2
total 233 263 88.5


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2010-2014 -- leonerd@leonerd.org.uk
5              
6             package Protocol::IRC::Client;
7              
8 10     10   121767 use strict;
  10         22  
  10         372  
9 10     10   48 use warnings;
  10         24  
  10         266  
10 10     10   343 use 5.010; # //
  10         32  
  10         469  
11 10     10   47 use base qw( Protocol::IRC );
  10         16  
  10         3845  
12              
13             our $VERSION = '0.10';
14              
15 10     10   81 use Carp;
  10         19  
  10         23777  
16              
17             =head1 NAME
18              
19             C - IRC protocol handling for a client
20              
21             =head1 DESCRIPTION
22              
23             This mix-in class provides a layer of IRC message handling logic suitable for
24             an IRC client. It builds upon L to provide extra message
25             processing useful to IRC clients, such as handling inbound server numerics.
26              
27             It provides some of the methods required by C:
28              
29             =over 4
30              
31             =item * isupport
32              
33             =back
34              
35             =cut
36              
37             =head1 METHODS
38              
39             =cut
40              
41             =head2 $value = $irc->isupport( $key )
42              
43             Returns an item of information from the server's C<005 ISUPPORT> lines.
44             Traditionally IRC servers use all-capital names for keys.
45              
46             =cut
47              
48             # A few hardcoded defaults from RFC 2812
49             my %ISUPPORT = (
50             channame_re => qr/^[#&]/,
51             prefixflag_re => qr/^[\@+]/,
52             chanmodes_list => [qw( b k l imnpst )], # TODO: ov
53             );
54              
55             sub isupport
56             {
57 236     236 1 257 my $self = shift;
58 236         267 my ( $field ) = @_;
59 236   100     1830 return $self->{Protocol_IRC_isupport}->{$field} // $ISUPPORT{$field};
60             }
61              
62             sub on_message_RPL_ISUPPORT
63             {
64 2     2 0 5 my $self = shift;
65 2         5 my ( $message, $hints ) = @_;
66              
67 2   50     18 my $isupport = $self->{Protocol_IRC_isupport} ||= {};
68              
69 2         4 foreach my $entry ( @{ $hints->{isupport} } ) {
  2         6  
70 9         44 my ( $name, $value ) = $entry =~ m/^([A-Z]+)(?:=(.*))?$/;
71              
72 9 100       23 $value = 1 if !defined $value;
73              
74 9         25 $isupport->{$name} = $value;
75              
76 9 100       38 if( $name eq "PREFIX" ) {
    100          
    100          
    100          
77 2         3 my $prefix = $value;
78              
79 2 50       49 my ( $prefix_modes, $prefix_flags ) = $prefix =~ m/^\(([a-z]+)\)(.+)$/i
80             or warn( "Unable to parse PREFIX=$value" ), next;
81              
82 2         6 $isupport->{prefix_modes} = $prefix_modes;
83 2         5 $isupport->{prefix_flags} = $prefix_flags;
84              
85 2         57 $isupport->{prefixflag_re} = qr/[$prefix_flags]/;
86              
87 2         4 my %prefix_map;
88 2         19 $prefix_map{substr $prefix_modes, $_, 1} = substr $prefix_flags, $_, 1 for ( 0 .. length($prefix_modes) - 1 );
89              
90 2         5 $isupport->{prefix_map_m2f} = \%prefix_map;
91 2         16 $isupport->{prefix_map_f2m} = { reverse %prefix_map };
92             }
93             elsif( $name eq "CHANMODES" ) {
94 2         12 $isupport->{chanmodes_list} = [ split( m/,/, $value ) ];
95             }
96             elsif( $name eq "CASEMAPPING" ) {
97             # TODO
98             # $self->{nick_folded} = $self->casefold_name( $self->{nick} );
99             }
100             elsif( $name eq "CHANTYPES" ) {
101 1         15 $isupport->{channame_re} = qr/^[$value]/;
102             }
103             }
104              
105 2         10 return 0;
106             }
107              
108             =head2 $info = $irc->server_info( $key )
109              
110             Returns an item of information from the server's C<004> line. C<$key> should
111             one of
112              
113             =over 8
114              
115             =item * host
116              
117             =item * version
118              
119             =item * usermodes
120              
121             =item * channelmodes
122              
123             =back
124              
125             =cut
126              
127             sub server_info
128             {
129 1     1 1 10 my $self = shift;
130 1         3 my ( $key ) = @_;
131              
132 1         8 return $self->{Protocol_IRC_server_info}{$key};
133             }
134              
135             sub on_message_RPL_MYINFO
136             {
137 1     1 0 2 my $self = shift;
138 1         2 my ( $message, $hints ) = @_;
139              
140 1         6 @{$self->{Protocol_IRC_server_info}}{qw( host version usermodes channelmodes )} =
  1         3  
141 1         2 @{$hints}{qw( serverhost serverversion usermodes channelmodes )};
142              
143 1         7 return 0;
144             }
145              
146             =head1 GATING MESSAGES
147              
148             If messages with a gating disposition are received, extra processing is
149             applied. Messages whose gating effect is C are simply collected up by
150             pushing the hints hash to an array. Added to this hash is the command name
151             itself, so that in the case of multiple message types (for example C
152             replies) the individual messages can still be identified.
153              
154             When the effect of C or C is eventually received, this collected
155             array is passed as C<$data> to a handler in one of the following places:
156              
157             =over 4
158              
159             =item 1.
160              
161             A method called C
162              
163             $client->on_gate_EFFECT_GATE( $message, $hints, $data )
164              
165             =item 2.
166              
167             A method called C
168              
169             $client->on_gate_EFFECT( 'GATE', $message, $hints, $data )
170              
171             =item 3.
172              
173             A method called C
174              
175             $client->on_gate( 'EFFECT, 'GATE', $message, $hints, $data )
176              
177             =back
178              
179             =cut
180              
181             sub on_message_gate
182             {
183 15     15 0 17 my $self = shift;
184 15         19 my ( $effect, $gate, $message, $hints ) = @_;
185 15   100     40 my $target = $hints->{target_name_folded} // "*";
186              
187 15 100       28 if( $effect eq "more" ) {
188 10         9 push @{ $self->{Protocol_IRC_gate}{$target}{$gate} }, {
  10         67  
189             %$hints,
190             command => $message->command_name,
191             };
192 10         62 return 1;
193             }
194              
195 5         13 my $data = delete $self->{Protocol_IRC_gate}{$target}{$gate};
196 5 50       7 keys %{ $self->{Protocol_IRC_gate}{$target} } or delete $self->{Protocol_IRC_gate}{$target};
  5         38  
197              
198 5         55 my %hints = (
199             %$hints,
200             synthesized => 1,
201             );
202              
203 5 50       26 $self->invoke( "on_gate_${effect}_$gate", $message, \%hints, $data ) and $hints{handled} = 1;
204 5 50       52 $self->invoke( "on_gate_$effect", $gate, $message, \%hints, $data ) and $hints{handled} = 1;
205 5 50       16 $self->invoke( "on_gate", $effect, $gate, $message, \%hints, $data ) and $hints{handled} = 1;
206              
207 5         50 return $hints{handled};
208             }
209              
210             =head1 INTERNAL MESSAGE HANDLING
211              
212             The following messages are handled internally by C.
213              
214             =cut
215              
216             sub _invoke_synthetic
217             {
218 5     5   18 my $self = shift;
219 5         11 my ( $command, $message, $hints, @morehints ) = @_;
220              
221 5         74 my %hints = (
222             %$hints,
223             synthesized => 1,
224             @morehints,
225             );
226 5         11 delete $hints{handled};
227              
228 5 50       22 $self->invoke( "on_message_$command", $message, \%hints ) and $hints{handled} = 1;
229 5 50       16 $self->invoke( "on_message", $command, $message, \%hints ) and $hints{handled} = 1;
230              
231 5         66 return $hints{handled};
232             }
233              
234             =head2 CAP
235              
236             This message takes a sub-verb as its second argument, and a list of capability
237             names as its third. On receipt of a C message, the verb is extracted and
238             set as the C hint, and the list capabilities set as the keys of a hash
239             given as the C hint. These are then passed to an event called
240              
241             $irc->on_message_cap_VERB( $message, \%hints )
242              
243             or
244              
245             $irc->on_message_cap( 'VERB', $message, \%hints )
246              
247             =cut
248              
249             sub on_message_CAP
250             {
251 3     3 0 36 my $self = shift;
252 3         5 my ( $message, $hints ) = @_;
253              
254 3         12 my $verb = $message->arg(1);
255              
256 5         51 my %hints = (
257             %$hints,
258             verb => $verb,
259 3         21 caps => { map { $_ => 1 } split m/ /, $message->arg(2) },
260             );
261              
262 3 100       18 $self->invoke( "on_message_cap_$verb", $message, \%hints ) and $hints{handled} = 1;
263 3 100       11 $self->invoke( "on_message_cap", $verb, $message, \%hints ) and $hints{handled} = 1;
264              
265 3         60 return $hints{handled};
266             }
267              
268             =head2 MODE (on channels) and 324 (RPL_CHANNELMODEIS)
269              
270             These message involve channel modes. The raw list of channel modes is parsed
271             into an array containing one entry per affected piece of data. Each entry will
272             contain at least a C key, indicating what sort of mode or mode change
273             it is:
274              
275             =over 8
276              
277             =item list
278              
279             The mode relates to a list; bans, invites, etc..
280              
281             =item value
282              
283             The mode sets a value about the channel
284              
285             =item bool
286              
287             The mode is a simple boolean flag about the channel
288              
289             =item occupant
290              
291             The mode relates to a user in the channel
292              
293             =back
294              
295             Every mode type then provides a C key, containing the mode character
296             itself, and a C key which is an empty string, C<+>, or C<->.
297              
298             For C and C types, the C key gives the actual list entry
299             or value being set.
300              
301             For C types, a C key gives the mode converted into an occupant
302             flag (by the C method), C and C store the
303             user name affected.
304              
305             C types do not create any extra keys.
306              
307             =cut
308              
309             sub prepare_hints_channelmode
310             {
311 12     12 0 14 my $self = shift;
312 12         14 my ( $message, $hints ) = @_;
313              
314 12         12 my ( $listmodes, $argmodes, $argsetmodes, $boolmodes ) = @{ $self->isupport( 'chanmodes_list' ) };
  12         17  
315              
316 12         18 my $modechars = $hints->{modechars};
317 12         12 my @modeargs = @{ $hints->{modeargs} };
  12         25  
318              
319 12         12 my @modes; # [] -> { type => $, sense => $, mode => $, arg => $ }
320              
321 12         14 my $sense = 0;
322 12         30 foreach my $modechar ( split( m//, $modechars ) ) {
323 29 100       53 $sense = 1, next if $modechar eq "+";
324 22 100       39 $sense = -1, next if $modechar eq "-";
325              
326 16         14 my $hasarg;
327              
328 16         46 my $mode = {
329             mode => $modechar,
330             sense => $sense,
331             };
332              
333 16 100       91 if( index( $listmodes, $modechar ) > -1 ) {
    100          
    100          
    100          
    50          
334 2         2 $mode->{type} = 'list';
335 2 50       13 $mode->{value} = shift @modeargs if ( $sense != 0 );
336             }
337             elsif( index( $argmodes, $modechar ) > -1 ) {
338 2         3 $mode->{type} = 'value';
339 2 50       9 $mode->{value} = shift @modeargs if ( $sense != 0 );
340             }
341             elsif( index( $argsetmodes, $modechar ) > -1 ) {
342 4         8 $mode->{type} = 'value';
343 4 100       12 $mode->{value} = shift @modeargs if ( $sense > 0 );
344             }
345             elsif( index( $boolmodes, $modechar ) > -1 ) {
346 3         11 $mode->{type} = 'bool';
347             }
348             elsif( my $flag = $self->prefix_mode2flag( $modechar ) ) {
349 5         24 $mode->{type} = 'occupant';
350 5         9 $mode->{flag} = $flag;
351 5 50       16 $mode->{nick} = shift @modeargs if ( $sense != 0 );
352 5         14 $mode->{nick_folded} = $self->casefold_name( $mode->{nick} );
353             }
354             else {
355             # TODO: Err... not recognised ... what do I do?
356             }
357              
358             # TODO: Consider a per-mode event here...
359              
360 16         42 push @modes, $mode;
361             }
362              
363 12         49 $hints->{modes} = \@modes;
364             }
365              
366             sub prepare_hints_MODE
367             {
368 12     12 0 13 my $self = shift;
369 12         15 my ( $message, $hints ) = @_;
370              
371 12 50       30 if( $hints->{target_type} eq "channel" ) {
372 12         27 $self->prepare_hints_channelmode( $message, $hints );
373             }
374             }
375              
376             sub prepare_hints_RPL_CHANNELMODEIS
377             {
378 0     0 0 0 my $self = shift;
379 0         0 my ( $message, $hints ) = @_;
380              
381 0         0 $self->prepare_hints_channelmode( $message, $hints );
382             }
383              
384             =head2 RPL_WHOREPLY and RPL_ENDOFWHO
385              
386             These messages will be collected up, per channel, and formed into a single
387             synthesized event called C.
388              
389             Its hints hash will contain an extra key, C, which will be an ARRAY ref
390             containing the lines of the WHO reply. Each line will be a HASH reference
391             containing:
392              
393             =over 8
394              
395             =item user_ident
396              
397             =item user_host
398              
399             =item user_server
400              
401             =item user_nick
402              
403             =item user_nick_folded
404              
405             =item user_flags
406              
407             =back
408              
409             =cut
410              
411             sub on_gate_done_who
412             {
413 1     1 0 2 my $self = shift;
414 1         2 my ( $message, $hints, $data ) = @_;
415              
416 1         1 my @who = map {
417 1         2 my $b = $_;
418 1         2 +{ map { $_ => $b->{$_} } qw( user_ident user_host user_server user_nick user_nick_folded user_flags ) }
  6         15  
419             } @$data;
420              
421 1         3 $self->_invoke_synthetic( "who", $message, $hints,
422             who => \@who,
423             );
424             }
425              
426             =head2 RPL_NAMEREPLY and RPL_ENDOFNAMES
427              
428             These messages will be collected up, per channel, and formed into a single
429             synthesized event called C.
430              
431             Its hints hash will contain an extra key, C, which will be an ARRAY ref
432             containing the usernames in the channel. Each will be a HASH reference
433             containing:
434              
435             =over 8
436              
437             =item nick
438              
439             =item flag
440              
441             =back
442              
443             =cut
444              
445             sub on_gate_done_names
446             {
447 1     1 0 2 my $self = shift;
448 1         2 my ( $message, $hints, $data ) = @_;
449              
450 1         2 my @names = map { @{ $_->{names} } } @$data;
  1         2  
  1         13  
451              
452 1         3 my $prefixflag_re = $self->isupport( 'prefixflag_re' );
453 1         45 my $re = qr/^($prefixflag_re)?(.*)$/;
454              
455 1         2 my %names;
456              
457 1         2 foreach my $name ( @names ) {
458 3 50       22 my ( $flag, $nick ) = $name =~ $re or next;
459              
460 3   100     10 $flag ||= ''; # make sure it's defined
461              
462 3         13 $names{ $self->casefold_name( $nick ) } = { nick => $nick, flag => $flag };
463             }
464              
465 1         8 $self->_invoke_synthetic( "names", $message, $hints,
466             names => \%names,
467             );
468             }
469              
470             =head2 RPL_BANLIST and RPL_ENDOFBANS
471              
472             These messages will be collected up, per channel, and formed into a single
473             synthesized event called C.
474              
475             Its hints hash will contain an extra key, C, which will be an ARRAY ref
476             containing the ban lines. Each line will be a HASH reference containing:
477              
478             =over 8
479              
480             =item mask
481              
482             User mask of the ban
483              
484             =item by_nick
485              
486             =item by_nick_folded
487              
488             Nickname of the user who set the ban
489              
490             =item timestamp
491              
492             UNIX timestamp the ban was created
493              
494             =back
495              
496             =cut
497              
498             sub on_gate_done_bans
499             {
500 1     1 0 2 my $self = shift;
501 1         2 my ( $message, $hints, $data ) = @_;
502              
503 2         3 my @bans = map {
504 1         2 my $b = $_;
505 2         4 +{ map { $_ => $b->{$_} } qw( mask by_nick by_nick_folded timestamp ) }
  8         17  
506             } @$data;
507              
508 1         3 $self->_invoke_synthetic( "bans", $message, $hints,
509             bans => \@bans,
510             );
511             }
512              
513             =head2 RPL_MOTD, RPL_MOTDSTART and RPL_ENDOFMOTD
514              
515             These messages will be collected up into a synthesized event called C.
516              
517             Its hints hash will contain an extra key, C, which will be an ARRAY ref
518             containing the lines of the MOTD.
519              
520             =cut
521              
522             sub on_gate_done_motd
523             {
524 1     1 0 2 my $self = shift;
525 1         3 my ( $message, $hints, $data ) = @_;
526              
527 2         13 $self->_invoke_synthetic( "motd", $message, $hints,
528 1         3 motd => [ map { $_->{text} } @$data ],
529             );
530             }
531              
532             =head2 RPL_WHOIS* and RPL_ENDOFWHOIS
533              
534             These messages will be collected up into a synthesized event called C.
535              
536             Each C reply will be stripped of the standard hints hash keys,
537             leaving whatever remains. Added to this will be a key called C, whose
538             value will be the command name, minus the leading C, and converted
539             to lowercase.
540              
541             =cut
542              
543             sub on_gate_done_whois
544             {
545 1     1 0 2 my $self = shift;
546 1         2 my ( $message, $hints, $data ) = @_;
547              
548 1         2 my @whois;
549             my $channels;
550              
551 1         3 foreach my $h ( @$data ) {
552             # Just delete all the standard hints from each one
553 4         13 delete @{$h}{keys %$hints};
  4         19  
554 4         26 ( $h->{whois} = lc delete $h->{command} ) =~ s/^rpl_whois//;
555              
556             # Combine all the 'channels' results into one list
557 4 100       14 if( $h->{whois} eq "channels" ) {
558 2 100       7 if( $channels ) {
559 1         1 push @{$channels->{channels}}, @{$h->{channels}};
  1         3  
  1         9  
560 1         4 next;
561             }
562 1         2 $channels = $h;
563             }
564              
565 3         6 push @whois, $h;
566             }
567              
568 1         4 $self->_invoke_synthetic( "whois", $message, $hints, whois => \@whois );
569             }
570              
571             =head1 AUTHOR
572              
573             Paul Evans
574              
575             =cut
576              
577             0x55AA;