File Coverage

blib/lib/Net/Async/IRC.pm
Criterion Covered Total %
statement 163 187 87.1
branch 28 48 58.3
condition 24 48 50.0
subroutine 29 35 82.8
pod 9 15 60.0
total 253 333 75.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, 2008-2021 -- leonerd@leonerd.org.uk
5              
6             package Net::Async::IRC 0.12;
7              
8 8     8   1135186 use v5.14;
  8         57  
9 8     8   54 use warnings;
  8         16  
  8         281  
10              
11             # We need to use C3 MRO to make the ->isupport etc.. methods work properly
12 8     8   45 use mro 'c3';
  8         15  
  8         70  
13 8     8   327 use base qw( Net::Async::IRC::Protocol Protocol::IRC::Client );
  8         17  
  8         4116  
14              
15 8     8   27870 use Future::AsyncAwait;
  8         26105  
  8         43  
16              
17 8     8   398 use Carp;
  8         15  
  8         538  
18              
19 8     8   56 use List::Util 1.33 qw( any );
  8         148  
  8         509  
20 8     8   59 use Socket qw( SOCK_STREAM );
  8         17  
  8         397  
21              
22 8     8   4384 use MIME::Base64 qw( encode_base64 decode_base64 );
  8         6094  
  8         584  
23              
24 8     8   63 use constant HAVE_MSWIN32 => ( $^O eq "MSWin32" );
  8         18  
  8         20899  
25              
26             =head1 NAME
27              
28             C - use IRC with C
29              
30             =head1 SYNOPSIS
31              
32             use Future::AsyncAwait;
33              
34             use IO::Async::Loop;
35             use Net::Async::IRC;
36              
37             my $loop = IO::Async::Loop->new;
38              
39             my $irc = Net::Async::IRC->new(
40             on_message_text => sub {
41             my ( $self, $message, $hints ) = @_;
42              
43             print "$hints->{prefix_name} says: $hints->{text}\n";
44             },
45             );
46              
47             $loop->add( $irc );
48              
49             await $irc->login(
50             nick => "MyName",
51             host => "irc.example.org",
52             );
53              
54             await $irc->do_PRIVMSG( target => "YourName", text => "Hello world!" );
55              
56             $loop->run;
57              
58             =head1 DESCRIPTION
59              
60             This object class implements an asynchronous IRC client, for use in programs
61             based on L.
62              
63             Most of the actual IRC message handling behaviour is implemented by the parent
64             class L.
65              
66             Most of the behaviour related to being an IRC client is implemented by the
67             parent class L.
68              
69             The following documentation may make mention of these above two parent
70             classes; the reader should make reference to them when required.
71              
72             =cut
73              
74             sub new
75             {
76 9     9 1 132936 my $class = shift;
77 9         52 my %args = @_;
78              
79 9         29 my $on_closed = delete $args{on_closed};
80              
81             return $class->SUPER::new(
82             %args,
83              
84             on_closed => sub {
85 0     0   0 my $self = shift;
86              
87 0 0       0 if( $self->{on_login_f} ) {
88 0         0 $_->fail( "Closed" ) for @{ $self->{on_login_f} };
  0         0  
89 0         0 undef $self->{on_login_f};
90             }
91              
92 0 0       0 $on_closed->( $self ) if $on_closed;
93             },
94 9         150 );
95             }
96              
97             sub _init
98             {
99 9     9   117 my $self = shift;
100 9         60 $self->SUPER::_init( @_ );
101              
102             $self->{user} = $ENV{LOGNAME} ||
103 9   33     6089 ( HAVE_MSWIN32 ? Win32::LoginName() : getpwuid($>) );
104              
105 9         39 our $VERSION;
106 9         112 $self->{realname} = "Net::Async::IRC client $VERSION";
107             }
108              
109             =head1 PARAMETERS
110              
111             The following named parameters may be passed to C or C:
112              
113             =over 8
114              
115             =item nick => STRING
116              
117             =item user => STRING
118              
119             =item realname => STRING
120              
121             Connection details. See also C, C.
122              
123             If C is not supplied, it will default to either C<$ENV{LOGNAME}> or the
124             current user's name as supplied by C or C.
125              
126             If unconnected, changing these properties will set the default values to use
127             when logging in.
128              
129             If logged in, changing the C property is equivalent to calling
130             C. Changing the other properties will not take effect until the
131             next login.
132              
133             =item use_caps => ARRAY of STRING
134              
135             Attempts to negotiate IRC v3.1 CAP at connect time. The array gives the names
136             of capabilities which will be requested, if the server supports them.
137              
138             If the C capability is requested and supported by the server, the
139             C method will use that.
140              
141             =back
142              
143             =cut
144              
145             sub configure
146             {
147 11     11 1 12090 my $self = shift;
148 11         71 my %args = @_;
149              
150 11         39 for (qw( user realname use_caps )) {
151 33 100       114 $self->{$_} = delete $args{$_} if exists $args{$_};
152             }
153              
154 11 100       53 if( exists $args{nick} ) {
155 1         18 $self->_set_nick( delete $args{nick} );
156             }
157              
158 11         138 $self->SUPER::configure( %args );
159             }
160              
161             =head1 METHODS
162              
163             The following methods documented in an C expression return L
164             instances.
165              
166             =cut
167              
168             =head2 connect
169              
170             $irc = await $irc->connect( %args );
171              
172             Connects to the IRC server. This method does not perform the complete IRC
173             login sequence; for that see instead the C method. The returned
174             L will yield the C<$irc> instance itself, to make chaining easier.
175              
176             =over 8
177              
178             =item host => STRING
179              
180             Hostname of the IRC server.
181              
182             =item service => STRING or NUMBER
183              
184             Optional. Port number or service name of the IRC server. Defaults to 6667.
185              
186             =back
187              
188             Any other arguments are passed into the underlying C
189             C method.
190              
191             $irc->connect( %args );
192              
193             The following additional arguments are used to provide continuations when not
194             returning a Future.
195              
196             =over 8
197              
198             =item on_connected => CODE
199              
200             Continuation to invoke once the connection has been established. Usually used
201             by the C method to perform the actual login sequence.
202              
203             $on_connected->( $irc )
204              
205             =item on_error => CODE
206              
207             Continuation to invoke in the case of an error preventing the connection from
208             taking place.
209              
210             $on_error->( $errormsg )
211              
212             =back
213              
214             =cut
215              
216             # TODO: Most of this needs to be moved into an abstract Net::Async::Connection role
217             sub connect
218             {
219 8     8 1 328 my $self = shift;
220 8         29 my %args = @_;
221              
222             # Largely for unit testing
223 8 100 66     50 return $self->{connect_f} ||= Future->new->done( $self ) if
224             $self->read_handle;
225              
226 2         14 my $on_error = delete $args{on_error};
227              
228 2   50     15 $args{service} ||= "6667";
229              
230             return $self->{connect_f} ||= $self->SUPER::connect(
231             %args,
232              
233             on_resolve_error => sub {
234 0     0   0 my ( $msg ) = @_;
235 0         0 chomp $msg;
236              
237 0 0       0 if( $args{on_resolve_error} ) {
    0          
238 0         0 $args{on_resolve_error}->( $msg );
239             }
240             elsif( $on_error ) {
241 0         0 $on_error->( "Cannot resolve - $msg" );
242             }
243             },
244              
245             on_connect_error => sub {
246 0 0   0   0 if( $args{on_connect_error} ) {
    0          
247 0         0 $args{on_connect_error}->( @_ );
248             }
249             elsif( $on_error ) {
250 0         0 $on_error->( "Cannot connect" );
251             }
252             },
253 2   33 0   96 )->on_fail( sub { undef $self->{connect_f} } );
  0         0  
254             }
255              
256             =head2 login
257              
258             $irc = await $irc->login( %args );
259              
260             Logs in to the IRC network, connecting first using the C method if
261             required. Takes the following named arguments:
262              
263             =over 8
264              
265             =item nick => STRING
266              
267             =item user => STRING
268              
269             =item realname => STRING
270              
271             IRC connection details. Defaults can be set with the C or C
272             methods.
273              
274             =item pass => STRING
275              
276             Server password to connect with.
277              
278             =back
279              
280             Any other arguments that are passed, are forwarded to the C method if
281             it is required; i.e. if C is invoked when not yet connected to the
282             server.
283              
284             $irc->login( %args );
285              
286             The following additional arguments are used to provide continuations when not
287             returning a Future.
288              
289             =over 8
290              
291             =item on_login => CODE
292              
293             A continuation to invoke once login is successful.
294              
295             $on_login->( $irc )
296              
297             =back
298              
299             If the C capability was requested and is supported by the server, this
300             will be used instead of the simple C command combination.
301              
302             At the current version, only the C SASL mechanism is supported.
303              
304             =cut
305              
306             sub login
307             {
308 6     6 1 4574 my $self = shift;
309 6         40 my %args = @_;
310              
311             return $self->{login_f} //= $self->_login( %args )
312 6   33 0   57 ->on_fail( sub { undef $self->{login_f} } );
  0         0  
313             }
314              
315             async sub _login
316 6     6   14 {
317 6         13 my $self = shift;
318 6         18 my %args = @_;
319              
320 6 50 66     33 my $nick = delete $args{nick} || $self->{nick} or croak "Need a login nick";
321 6 50 66     51 my $user = delete $args{user} || $self->{user} or croak "Need a login user";
322 6   66     26 my $realname = delete $args{realname} || $self->{realname};
323 6         14 my $pass = delete $args{pass};
324              
325 6 100       18 if( !defined $self->{nick} ) {
326 5         29 $self->_set_nick( $nick );
327             }
328              
329 6         160 my $on_login = delete $args{on_login};
330 6 50 66     48 !defined $on_login or ref $on_login eq "CODE" or
331             croak "Expected 'on_login' to be a CODE reference";
332              
333 6         33 await $self->connect( %args );
334              
335 6         609 undef $self->{defer_cap_end_f};
336 6         17 undef $self->{on_cap_finished_f};
337              
338 6 100       57 $self->send_message( "CAP", undef, "LS" ) if $self->{use_caps};
339              
340 6         1366 $self->send_message( "NICK", undef, $nick );
341              
342 6         1010 my $use_sasl;
343              
344 6 100 100 4   83 if( $self->{use_caps} and any { $_ eq "sasl" } @{ $self->{use_caps} } ) {
  4         24  
  4         42  
345 2         27 await ( $self->{on_cap_finished_f} //= $self->loop->new_future );
346              
347 2         188 $use_sasl = $self->cap_supported( "sasl" );
348             }
349              
350 6 100       30 if( $use_sasl ) {
351 1         3 push @{ $self->{defer_cap_end_f} }, my $f = $self->loop->new_future;
  1         5  
352 1         29 undef $self->{on_sasl_complete_f};
353              
354 1         5 $self->send_message( "USER", undef, $user, "0", "*", $realname );
355              
356             # TODO: configurable mechanisms
357 1         233 $self->send_message( "AUTHENTICATE", undef, "PLAIN" );
358 1         110 await ( $self->{on_authenticate_f} //= $self->loop->new_future );
359              
360 1         85 my $payload = encode_base64( join( "\0", $nick, $nick, $pass ), "" );
361 1         4 $self->send_message( "AUTHENTICATE", undef, $payload );
362              
363 1         259 await ( $self->{on_sasl_complete_f} //= $self->loop->new_future );
364              
365 1         82 $f->done;
366             }
367             else {
368 5 100       26 $self->send_message( "PASS", undef, $pass ) if defined $pass;
369 5         131 $self->send_message( "USER", undef, $user, "0", "*", $realname );
370             }
371              
372 6         986 my $f = $self->loop->new_future;
373              
374 6         2400 push @{ $self->{on_login_f} }, $f;
  6         21  
375 6 100       27 $f->on_done( $on_login ) if $on_login;
376              
377 6         56 return await $f;
378             }
379              
380             =head2 change_nick
381              
382             $irc->change_nick( $newnick );
383              
384             Requests to change the nick. If unconnected, the change happens immediately
385             to the stored defaults. If logged in, sends a C command to the server,
386             which may suceed or fail at a later point.
387              
388             =cut
389              
390             sub change_nick
391             {
392 3     3 1 41 my $self = shift;
393 3         6 my ( $newnick ) = @_;
394              
395 3 50       12 if( !$self->is_connected ) {
396 0         0 $self->_set_nick( $newnick );
397             }
398             else {
399 3         50 $self->send_message( "NICK", undef, $newnick );
400             }
401             }
402              
403             ############################
404             # Message handling methods #
405             ############################
406              
407             =head1 IRC v3.1 CAPABILITIES
408              
409             The following methods relate to IRC v3.1 capabilities negotiations.
410              
411             =cut
412              
413             sub on_message_cap_LS
414             {
415 3     3 0 48 my $self = shift;
416 3         8 my ( $message, $hints ) = @_;
417              
418 3         18 my $supported = $self->{caps_supported} = $hints->{caps};
419              
420 3         7 my @request = grep { $supported->{$_} } @{$self->{use_caps}};
  3         10  
  3         8  
421              
422 3 100       10 if( @request ) {
423 2         12 $self->{caps_enabled} = { map { $_ => undef } @request };
  2         22  
424 2         15 $self->send_message( "CAP", undef, "REQ", join( " ", @request ) );
425             }
426             else {
427 1         5 $self->send_message( "CAP", undef, "END" );
428 1   33     211 ( $self->{on_cap_finished_f} //= $self->loop->new_future )->done;
429             }
430              
431 3         509 return 1;
432             }
433              
434             *on_message_cap_ACK = *on_message_cap_NAK = \&_on_message_cap_reply;
435             sub _on_message_cap_reply
436             {
437 2     2   63 my $self = shift;
438 2         7 my ( $message, $hints ) = @_;
439 2         30 my $ack = $hints->{verb} eq "ACK";
440              
441 2         8 $self->{caps_enabled}{$_} = $ack for keys %{ $hints->{caps} };
  2         13  
442              
443             # Are any outstanding
444 2   50     4 !defined and return 1 for values %{ $self->{caps_enabled} };
  2         13  
445              
446 2   66     18 ( $self->{on_cap_finished_f} //= $self->loop->new_future )->done;
447              
448             $self->adopt_future(
449 2         13 Future->needs_all( @{ $self->{defer_cap_end_f} } )->then(
450 2     2   248 sub { $self->send_message( "CAP", undef, "END" ); }
451             )
452 2         164 );
453              
454 2         689 return 1;
455             }
456              
457             =head2 caps_supported
458              
459             $caps = $irc->caps_supported;
460              
461             Returns a HASH whose keys give the capabilities listed by the server as
462             supported in its C response. If the server ignored the C
463             negotiation then this method returns C.
464              
465             =cut
466              
467             sub caps_supported
468             {
469 3     3 1 3065 my $self = shift;
470 3         38 return $self->{caps_supported};
471             }
472              
473             =head2 cap_supported
474              
475             $supported = $irc->cap_supported( $cap );
476              
477             Returns a boolean indicating if the server supports the named capability.
478              
479             =cut
480              
481             sub cap_supported
482             {
483 5     5 1 1614 my $self = shift;
484 5         14 my ( $cap ) = @_;
485 5         24 return !!$self->{caps_supported}{$cap};
486             }
487              
488             =head2 caps_enabled
489              
490             $caps = $irc->caps_enabled;
491              
492             Returns a HASH whose keys give the capabilities successfully enabled by the
493             server as part of the C login sequence. If the server ignored the
494             C negotiation then this method returns C.
495              
496             =cut
497              
498             sub caps_enabled
499             {
500 3     3 1 6 my $self = shift;
501 3         17 return $self->{caps_enabled};
502             }
503              
504             =head2 cap_enabled
505              
506             $enabled = $irc->cap_enabled( $cap );
507              
508             Returns a boolean indicating if the client successfully enabled the named
509             capability.
510              
511             =cut
512              
513             sub cap_enabled
514             {
515 2     2 1 7 my $self = shift;
516 2         5 my ( $cap ) = @_;
517 2         10 return !!$self->{caps_enabled}{$cap};
518             }
519              
520             sub on_message_NICK
521             {
522 1     1 0 17 my $self = shift;
523 1         3 my ( $message, $hints ) = @_;
524              
525 1 50       4 if( $hints->{prefix_is_me} ) {
526 1         4 $self->_set_nick( $hints->{new_nick} );
527 1         20 return 1;
528             }
529              
530 0         0 return 0;
531             }
532              
533             sub on_message_RPL_WELCOME
534             {
535 6     6 0 131 my $self = shift;
536 6         53 my ( $message ) = @_;
537              
538             # set our nick to be what the server logged us in as
539 6         42 $self->_set_nick( $message->{args}[0] );
540              
541 6 50 33     149 if( $self->{on_login_f} and @{ $self->{on_login_f} } ) {
  6         27  
542 6         13 my @futures = @{ $self->{on_login_f} };
  6         18  
543 6         11 undef $self->{on_login_f};
544              
545 6         16 foreach my $f ( @futures ) {
546 6         28 $f->done( $self );
547             }
548             }
549              
550             # Don't eat it
551 6         1022 return 0;
552             }
553              
554             sub on_message_AUTHENTICATE
555             {
556 1     1 0 19 my $self = shift;
557 1         3 my ( $message ) = @_;
558              
559 1         4 my $data = $message->arg( 0 );
560              
561 1 50       10 if( $data eq "+" ) {
562             # Done
563 1   50     7 my $data = $self->{authenticate_buffer} // "";
564 1         3 my $f = $self->{on_authenticate_f};
565              
566 1         2 undef @{$self}{qw( authenticate_buffer on_authenticate_f )};
  1         3  
567              
568 1 50       7 $f->done( $self, $data ) if $f;
569 1         84 return 1;
570             }
571              
572 0         0 $self->{authenticate_buffer} .= decode_base64( $data );
573              
574 0         0 return 1;
575             }
576              
577             sub on_message_RPL_SASLSUCCESS
578             {
579 1     1 0 17 my $self = shift;
580 1         3 my ( $message, $hints ) = @_;
581              
582 1   33     7 ( $self->{on_sasl_complete_f} //= Future->new )->done( $message, $hints );
583              
584 1         41 return 0;
585             }
586              
587             sub on_message_ERR_SASLFAIL
588             {
589 0     0 0   my $self = shift;
590 0           my ( $message, $hints ) = @_;
591              
592             # It's still complete even though it failed
593 0   0       ( $self->{on_sasl_complete_f} //= Future->new )->done( $message, $hints );
594              
595 0           return 0;
596             }
597              
598             =head1 MESSAGE-WRAPPING METHODS
599              
600             The following methods are all inherited from L but are
601             mentioned again for convenient. For further details see the documentation in
602             the parent module.
603              
604             In particular, each method returns a L instance.
605              
606             =cut
607              
608             =head2 do_PRIVMSG
609              
610             =head2 do_NOTICE
611              
612             await $irc->do_PRIVMSG( target => $target, text => $text );
613              
614             await $irc->do_NOTICE( target => $target, text => $text );
615              
616             Sends a C or C command.
617              
618             =cut
619              
620             =head1 SEE ALSO
621              
622             =over 4
623              
624             =item *
625              
626             L - Internet Relay Chat: Client Protocol
627              
628             =back
629              
630             =head1 AUTHOR
631              
632             Paul Evans
633              
634             =cut
635              
636             0x55AA;