File Coverage

blib/lib/Protocol/IRC/Client.pm
Criterion Covered Total %
statement 177 184 96.2
branch 55 70 78.5
condition 14 19 73.6
subroutine 23 25 92.0
pod 5 17 29.4
total 274 315 86.9


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-2017 -- leonerd@leonerd.org.uk
5              
6             package Protocol::IRC::Client;
7              
8 7     7   96606 use strict;
  7         9  
  7         190  
9 7     7   26 use warnings;
  7         9  
  7         190  
10 7     7   149 use 5.010; # //
  7         19  
11 7     7   29 use base qw( Protocol::IRC );
  7         22  
  7         2943  
12              
13             our $VERSION = '0.12';
14              
15 7     7   37 use Carp;
  7         7  
  7         7050  
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 INHERITED METHODS
38              
39             The following methods, inherited from L, are notable here as
40             being particularly useful for a client.
41              
42             =head2 send_message
43              
44             $irc->send_message( $message )
45             $irc->send_message( $command, { %args } )
46             $irc->send_message( $command, $prefix, @args )
47              
48             See L
49              
50             =cut
51              
52             =head1 METHODS
53              
54             =cut
55              
56             =head2 isupport
57              
58             $value = $irc->isupport( $key )
59              
60             Returns an item of information from the server's C<005 ISUPPORT> lines.
61             Traditionally IRC servers use all-capital names for keys.
62              
63             =cut
64              
65             # A few hardcoded defaults from RFC 2812
66             my %ISUPPORT = (
67             channame_re => qr/^[#&]/,
68             prefixflag_re => qr/^[\@+]/,
69             chanmodes_list => [qw( b k l imnpst )], # TODO: ov
70             );
71              
72             sub isupport
73             {
74 222     222 1 154 my $self = shift;
75 222         153 my ( $field ) = @_;
76 222   66     1062 return $self->{Protocol_IRC_isupport}->{$field} // $ISUPPORT{$field};
77             }
78              
79             sub on_message_RPL_ISUPPORT
80             {
81 2     2 0 3 my $self = shift;
82 2         3 my ( $message, $hints ) = @_;
83              
84 2   50     6 my $isupport = $self->{Protocol_IRC_isupport} ||= {};
85              
86 2         3 foreach my $entry ( @{ $hints->{isupport} } ) {
  2         5  
87 9         38 my ( $name, $value ) = $entry =~ m/^([A-Z]+)(?:=(.*))?$/;
88              
89 9 100       16 $value = 1 if !defined $value;
90              
91 9         15 $isupport->{$name} = $value;
92              
93 9 100       28 if( $name eq "PREFIX" ) {
    100          
    100          
    100          
94 2         25 my $prefix = $value;
95              
96 2 50       11 my ( $prefix_modes, $prefix_flags ) = $prefix =~ m/^\(([a-z]+)\)(.+)$/i
97             or warn( "Unable to parse PREFIX=$value" ), next;
98              
99 2         5 $isupport->{prefix_modes} = $prefix_modes;
100 2         4 $isupport->{prefix_flags} = $prefix_flags;
101              
102 2         31 $isupport->{prefixflag_re} = qr/[$prefix_flags]/;
103              
104 2         2 my %prefix_map;
105 2         19 $prefix_map{substr $prefix_modes, $_, 1} = substr $prefix_flags, $_, 1 for ( 0 .. length($prefix_modes) - 1 );
106              
107 2         3 $isupport->{prefix_map_m2f} = \%prefix_map;
108 2         12 $isupport->{prefix_map_f2m} = { reverse %prefix_map };
109             }
110             elsif( $name eq "CHANMODES" ) {
111 2         9 $isupport->{chanmodes_list} = [ split( m/,/, $value ) ];
112             }
113             elsif( $name eq "CASEMAPPING" ) {
114             # TODO
115             # $self->{nick_folded} = $self->casefold_name( $self->{nick} );
116             }
117             elsif( $name eq "CHANTYPES" ) {
118 1         9 $isupport->{channame_re} = qr/^[$value]/;
119             }
120             }
121              
122 2         8 return 0;
123             }
124              
125             =head2 server_info
126              
127             $info = $irc->server_info( $key )
128              
129             Returns an item of information from the server's C<004> line. C<$key> should
130             one of
131              
132             =over 8
133              
134             =item * host
135              
136             =item * version
137              
138             =item * usermodes
139              
140             =item * channelmodes
141              
142             =back
143              
144             =cut
145              
146             sub server_info
147             {
148 1     1 1 6 my $self = shift;
149 1         2 my ( $key ) = @_;
150              
151 1         5 return $self->{Protocol_IRC_server_info}{$key};
152             }
153              
154             sub on_message_RPL_MYINFO
155             {
156 1     1 0 1 my $self = shift;
157 1         4 my ( $message, $hints ) = @_;
158              
159 1         4 @{$self->{Protocol_IRC_server_info}}{qw( host version usermodes channelmodes )} =
160 1         2 @{$hints}{qw( serverhost serverversion usermodes channelmodes )};
  1         2  
161              
162 1         3 return 0;
163             }
164              
165             =head1 GATING MESSAGES
166              
167             If messages with a gating disposition are received, extra processing is
168             applied. Messages whose gating effect is C are simply collected up by
169             pushing the hints hash to an array. Added to this hash is the command name
170             itself, so that in the case of multiple message types (for example C
171             replies) the individual messages can still be identified.
172              
173             When the effect of C or C is eventually received, this collected
174             array is passed as C<$data> to a handler in one of the following places:
175              
176             =over 4
177              
178             =item 1.
179              
180             A method called C
181              
182             $client->on_gate_EFFECT_GATE( $message, $hints, $data )
183              
184             =item 2.
185              
186             A method called C
187              
188             $client->on_gate_EFFECT( 'GATE', $message, $hints, $data )
189              
190             =item 3.
191              
192             A method called C
193              
194             $client->on_gate( 'EFFECT, 'GATE', $message, $hints, $data )
195              
196             =item 4.
197              
198             If the gate effect is C, two more places are tried; looking like regular
199             event handling on a command whose name is the (lowercase) gate name
200              
201             $client->on_message_GATE( $message, $hints )
202              
203             $client->on_message( 'GATE', $message, $hints )
204              
205             =back
206              
207             For the following types of gate, the C<$data> is further processed in the
208             following way to provide extra hints fields.
209              
210             =cut
211              
212             sub on_message_gate
213             {
214 17     17 0 19 my $self = shift;
215 17         17 my ( $effect, $gate, $message, $hints ) = @_;
216 17   100     40 my $target = $hints->{target_name_folded} // "*";
217              
218 17 100       33 if( $effect eq "more" ) {
219 10         10 push @{ $self->{Protocol_IRC_gate}{$gate}{$target} }, {
  10         63  
220             %$hints,
221             command => $message->command_name,
222             };
223 10         55 return 1;
224             }
225              
226 7         17 my $data = delete $self->{Protocol_IRC_gate}{$gate}{$target};
227              
228 7         18 my @morehints;
229 7 100 100     67 if( $effect eq "done" and my $code = $self->can( "prepare_gatehints_$gate" ) ) {
230 5         40 @morehints = $self->$code( $data );
231             }
232              
233 7         57 my %hints = (
234             %$hints,
235             synthesized => 1,
236             @morehints,
237             );
238              
239 7         9 my $futures;
240 7 100 66     31 if( $futures = $self->{Protocol_IRC_gate_futures}{$gate}{$target} and @$futures ) {
241 1         1 my $f = shift @$futures;
242 1 50       3 if( $effect eq "done" ) {
243 1         6 $f->done( $message, \%hints, $data );
244             }
245             else {
246 0         0 $f->fail( $hints->{text}, irc_gate => $message, \%hints );
247             }
248             }
249              
250 7 50       74 $self->invoke( "on_gate_${effect}_$gate", $message, \%hints, $data ) and $hints{handled} = 1;
251 7 50       26 $self->invoke( "on_gate_$effect", $gate, $message, \%hints, $data ) and $hints{handled} = 1;
252 7 50       20 $self->invoke( "on_gate", $effect, $gate, $message, \%hints, $data ) and $hints{handled} = 1;
253              
254 7 100       61 if( $effect eq "done" ) {
255 6 50       21 $self->invoke( "on_message_$gate", $message, \%hints ) and $hints{handled} = 1;
256 6 50       21 $self->invoke( "on_message", $gate, $message, \%hints ) and $hints{handled} = 1;
257             }
258              
259 7         84 return $hints{handled};
260             }
261              
262             =head2 who
263              
264             The hints hash will contain an extra key, C, which will be an ARRAY ref
265             containing the lines of the WHO reply. Each line will be a HASH reference
266             containing:
267              
268             =over 8
269              
270             =item user_ident
271              
272             =item user_host
273              
274             =item user_server
275              
276             =item user_nick
277              
278             =item user_nick_folded
279              
280             =item user_flags
281              
282             =back
283              
284             =cut
285              
286             sub prepare_gatehints_who
287             {
288 1     1 0 5 my $self = shift;
289 1         2 my ( $data ) = @_;
290              
291             my @who = map {
292 1         3 my $b = $_;
  1         3  
293 1         3 +{ map { $_ => $b->{$_} } qw( user_ident user_host user_server user_nick user_nick_folded user_flags ) }
  6         50  
294             } @$data;
295              
296 1         5 return who => \@who;
297             }
298              
299             =head2 names
300              
301             The hints hash will contain an extra key, C, which will be an ARRAY ref
302             containing the usernames in the channel. Each will be a HASH reference
303             containing:
304              
305             =over 8
306              
307             =item nick
308              
309             =item flag
310              
311             =back
312              
313             =cut
314              
315             sub prepare_gatehints_names
316             {
317 1     1 0 2 my $self = shift;
318 1         2 my ( $data ) = @_;
319              
320 1         2 my @names = map { @{ $_->{names} } } @$data;
  1         1  
  1         4  
321              
322 1         3 my $prefixflag_re = $self->isupport( 'prefixflag_re' );
323 1         93 my $re = qr/^($prefixflag_re)?(.*)$/;
324              
325 1         2 my %names;
326              
327 1         3 foreach my $name ( @names ) {
328 3 50       19 my ( $flag, $nick ) = $name =~ $re or next;
329              
330 3   100     7 $flag ||= ''; # make sure it's defined
331              
332 3         9 $names{ $self->casefold_name( $nick ) } = { nick => $nick, flag => $flag };
333             }
334              
335 1         4 return names => \%names;
336             }
337              
338             =head2 bans
339              
340             The hints hash will contain an extra key, C, which will be an ARRAY ref
341             containing the ban lines. Each line will be a HASH reference containing:
342              
343             =over 8
344              
345             =item mask
346              
347             User mask of the ban
348              
349             =item by_nick
350              
351             =item by_nick_folded
352              
353             Nickname of the user who set the ban
354              
355             =item timestamp
356              
357             UNIX timestamp the ban was created
358              
359             =back
360              
361             =cut
362              
363             sub prepare_gatehints_bans
364             {
365 1     1 0 2 my $self = shift;
366 1         3 my ( $data ) = @_;
367              
368             my @bans = map {
369 1         4 my $b = $_;
  2         3  
370 2         4 +{ map { $_ => $b->{$_} } qw( mask by_nick by_nick_folded timestamp ) }
  8         24  
371             } @$data;
372              
373 1         5 return bans => \@bans;
374             }
375              
376             =head2 motd
377              
378             The hints hash will contain an extra key, C, which will be an ARRAY ref
379             containing the lines of the MOTD.
380              
381             =cut
382              
383             sub prepare_gatehints_motd
384             {
385 1     1 0 2 my $self = shift;
386 1         1 my ( $data ) = @_;
387              
388 1         3 return motd => [ map { $_->{text} } @$data ];
  2         8  
389             }
390              
391             =head2 whois
392              
393             The hints hash will contain an extra key, C, which will be an ARRAY ref
394             of entries that mostly relate to the received C numerics.
395              
396             Each C reply will be stripped of the standard hints hash keys,
397             leaving whatever remains. Added to this will be a key called C, whose
398             value will be the command name, minus the leading C, and converted
399             to lowercase.
400              
401             =cut
402              
403 7         6386 use constant STANDARD_HINTS => qw(
404             prefix_nick prefix_nick_folded
405             prefix_name prefix_name_folded
406             prefix_user
407             prefix_host
408             target_name target_name_folded
409             target_is_me
410             target_type
411             handled
412 7     7   41 );
  7         9  
413              
414             sub prepare_gatehints_whois
415             {
416 1     1 0 3 my $self = shift;
417 1         3 my ( $data ) = @_;
418              
419 1         1 my @whois;
420             my $channels;
421              
422 1         4 foreach my $h ( @$data ) {
423             # Just delete all the standard hints from each one
424 4         5 delete @{$h}{STANDARD_HINTS()};
  4         21  
425 4         24 ( $h->{whois} = lc delete $h->{command} ) =~ s/^rpl_whois//;
426              
427             # Combine all the 'channels' results into one list
428 4 100       11 if( $h->{whois} eq "channels" ) {
429 2 100       6 if( $channels ) {
430 1         2 push @{$channels->{channels}}, @{$h->{channels}};
  1         3  
  1         5  
431 1         2 next;
432             }
433 1         2 $channels = $h;
434             }
435              
436 3         5 push @whois, $h;
437             }
438              
439 1         4 return whois => \@whois;
440             }
441              
442             =head2 join
443              
444             No additional keys.
445              
446             =cut
447              
448             # TODO: maybe JOIN gate should wait for initial events?
449              
450             =head2 next_gate_future
451              
452             $f = $client->next_gate_future( $gate, $target )
453              
454             As an alternative to using the event handlers above, a client can instead
455             obtain a L that will succeed or fail the next time a result on a given
456             gate is received for a given target. This is often more convenient to use in a
457             client, as it represents the result of running a command.
458              
459             If the gate completes successfully, then so will the future, yielding the same
460             values as would be passed to the C event; namely that
461              
462             ( $message, $hints, $data ) = $f->get
463              
464             If the gate fails, then so will the future, containing the text message from
465             the error numeric as its failure message, C as its category, and the
466             full message and hints for it as the details.
467              
468             =cut
469              
470             sub next_gate_future
471             {
472 1     1 1 2149 my $self = shift;
473 1         2 my ( $gate, $target ) = @_;
474              
475 1   50     7 $target = $self->casefold_name( $target // "*" );
476              
477 1   50     8 my $futures = $self->{Protocol_IRC_gate_futures}{$gate}{$target} //= [];
478              
479 1         5 my $f = $self->new_future;
480              
481 1         16 push @$futures, $f;
482             $f->on_cancel( sub {
483 0     0   0 my ( $f ) = @_;
484 0         0 @$futures = grep { $_ != $f } @$futures
  0         0  
485 1         7 });
486              
487 1         38 return $f;
488             }
489              
490             =head1 INTERNAL MESSAGE HANDLING
491              
492             The following messages are handled internally by C.
493              
494             =cut
495              
496             =head2 CAP
497              
498             This message takes a sub-verb as its second argument, and a list of capability
499             names as its third. On receipt of a C message, the verb is extracted and
500             set as the C hint, and the list capabilities set as the keys of a hash
501             given as the C hint. These are then passed to an event called
502              
503             $irc->on_message_cap_VERB( $message, \%hints )
504              
505             or
506              
507             $irc->on_message_cap( 'VERB', $message, \%hints )
508              
509             =cut
510              
511             sub on_message_CAP
512             {
513 1     1 0 2 my $self = shift;
514 1         1 my ( $message, $hints ) = @_;
515              
516 1         4 my $verb = $message->arg(1);
517              
518             my %hints = (
519             %$hints,
520             verb => $verb,
521 1         4 caps => { map { $_ => 1 } split m/ /, $message->arg(2) },
  2         12  
522             );
523              
524 1 50       5 $self->invoke( "on_message_cap_$verb", $message, \%hints ) and $hints{handled} = 1;
525 1 50       3 $self->invoke( "on_message_cap", $verb, $message, \%hints ) and $hints{handled} = 1;
526              
527 1         10 return $hints{handled};
528             }
529              
530             =head2 MODE (on channels) and 324 (RPL_CHANNELMODEIS)
531              
532             These messages involve channel modes. The raw list of channel modes is parsed
533             into an array containing one entry per affected piece of data. Each entry will
534             contain at least a C key, indicating what sort of mode or mode change
535             it is:
536              
537             =over 8
538              
539             =item list
540              
541             The mode relates to a list; bans, invites, etc..
542              
543             =item value
544              
545             The mode sets a value about the channel
546              
547             =item bool
548              
549             The mode is a simple boolean flag about the channel
550              
551             =item occupant
552              
553             The mode relates to a user in the channel
554              
555             =back
556              
557             Every mode type then provides a C key, containing the mode character
558             itself, and a C key which is an empty string, C<+>, or C<->.
559              
560             For C and C types, the C key gives the actual list entry
561             or value being set.
562              
563             For C types, a C key gives the mode converted into an occupant
564             flag (by the C method), C and C store the
565             user name affected.
566              
567             C types do not create any extra keys.
568              
569             =cut
570              
571             sub prepare_hints_channelmode
572             {
573 12     12 0 6 my $self = shift;
574 12         11 my ( $message, $hints ) = @_;
575              
576 12         8 my ( $listmodes, $argmodes, $argsetmodes, $boolmodes ) = @{ $self->isupport( 'chanmodes_list' ) };
  12         13  
577              
578 12         12 my $modechars = $hints->{modechars};
579 12         11 my @modeargs = @{ $hints->{modeargs} };
  12         16  
580              
581 12         10 my @modes; # [] -> { type => $, sense => $, mode => $, arg => $ }
582              
583 12         10 my $sense = 0;
584 12         29 foreach my $modechar ( split( m//, $modechars ) ) {
585 29 100       43 $sense = 1, next if $modechar eq "+";
586 22 100       31 $sense = -1, next if $modechar eq "-";
587              
588 16         8 my $hasarg;
589              
590 16         27 my $mode = {
591             mode => $modechar,
592             sense => $sense,
593             };
594              
595 16 100       61 if( index( $listmodes, $modechar ) > -1 ) {
    100          
    100          
    100          
    50          
596 2         3 $mode->{type} = 'list';
597 2 50       11 $mode->{value} = shift @modeargs if ( $sense != 0 );
598             }
599             elsif( index( $argmodes, $modechar ) > -1 ) {
600 2         4 $mode->{type} = 'value';
601 2 50       8 $mode->{value} = shift @modeargs if ( $sense != 0 );
602             }
603             elsif( index( $argsetmodes, $modechar ) > -1 ) {
604 4         6 $mode->{type} = 'value';
605 4 100       9 $mode->{value} = shift @modeargs if ( $sense > 0 );
606             }
607             elsif( index( $boolmodes, $modechar ) > -1 ) {
608 3         5 $mode->{type} = 'bool';
609             }
610             elsif( my $flag = $self->prefix_mode2flag( $modechar ) ) {
611 5         6 $mode->{type} = 'occupant';
612 5         7 $mode->{flag} = $flag;
613 5 50       11 $mode->{nick} = shift @modeargs if ( $sense != 0 );
614 5         10 $mode->{nick_folded} = $self->casefold_name( $mode->{nick} );
615             }
616             else {
617             # TODO: Err... not recognised ... what do I do?
618             }
619              
620             # TODO: Consider a per-mode event here...
621              
622 16         22 push @modes, $mode;
623             }
624              
625 12         37 $hints->{modes} = \@modes;
626             }
627              
628             sub prepare_hints_MODE
629             {
630 12     12 0 11 my $self = shift;
631 12         10 my ( $message, $hints ) = @_;
632              
633 12 50       21 if( $hints->{target_type} eq "channel" ) {
634 12         21 $self->prepare_hints_channelmode( $message, $hints );
635             }
636             }
637              
638             sub prepare_hints_RPL_CHANNELMODEIS
639             {
640 0     0 0 0 my $self = shift;
641 0         0 my ( $message, $hints ) = @_;
642              
643 0         0 $self->prepare_hints_channelmode( $message, $hints );
644             }
645              
646             =head1 COMMAND-SENDING METHODS
647              
648             The following methods actually send IRC commands. Each is named after the
649             underlying IRC command it sends, using capital letters for methods that simply
650             send that command.
651              
652             =cut
653              
654             =head2 do_PRIVMSG
655              
656             =head2 do_NOTICE
657              
658             Sends a C or C command.
659              
660             For convenience, a single C argument may be provided which will be
661             renamed to C. If C is an ARRAY reference, it will be turned
662             into a comma-separated string.
663              
664             =cut
665              
666             sub _do_pmlike
667             {
668 6     6   7 my $self = shift;
669 6         6 my $command = shift;
670              
671 6         17 my %args = @_;
672              
673             my $targets =
674 2         4 ( ref $args{targets} eq "ARRAY" ) ? join( ",", @{ $args{targets} } ) :
675             ( defined $args{target} ) ? delete $args{target} :
676 6 100       19 $args{targets};
    100          
677              
678 6         26 $self->send_message( $command => { @_, targets => $targets } );
679             }
680              
681 3     3 1 1108 sub do_PRIVMSG { shift->_do_pmlike( PRIVMSG => @_ ) }
682 3     3 1 1084 sub do_NOTICE { shift->_do_pmlike( NOTICE => @_ ) }
683              
684             =head1 REQUIRED METHODS
685              
686             As this class is an abstract base class, a concrete implementation must
687             provide the following methods to complete it and make it useable.
688              
689             =cut
690              
691             =head2 new_future
692              
693             $f = $client->new_future
694              
695             Returns a new L instance or subclass thereof.
696              
697             =cut
698              
699             =head1 AUTHOR
700              
701             Paul Evans
702              
703             =cut
704              
705             0x55AA;