File Coverage

blib/lib/Protocol/IRC/Client.pm
Criterion Covered Total %
statement 174 181 96.1
branch 55 70 78.5
condition 14 19 73.6
subroutine 22 24 91.6
pod 5 17 29.4
total 270 311 86.8


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