File Coverage

blib/lib/Mojo/IRC.pm
Criterion Covered Total %
statement 185 216 85.6
branch 45 70 64.2
condition 18 36 50.0
subroutine 43 51 84.3
pod 17 18 94.4
total 308 391 78.7


line stmt bran cond sub pod time code
1             package Mojo::IRC;
2 11     11   1487321 use Mojo::Base 'Mojo::EventEmitter';
  11         828053  
  11         89  
3 11     11   22059 use Mojo::IOLoop;
  11         1608932  
  11         79  
4 11     11   564 use Mojo::Promise;
  11         32  
  11         70  
5 11     11   327 use File::Basename 'dirname';
  11         26  
  11         591  
6 11     11   71 use File::Spec::Functions 'catfile';
  11         23  
  11         517  
7 11     11   7054 use IRC::Utils ();
  11         107951  
  11         494  
8 11     11   7739 use Parse::IRC ();
  11         29602  
  11         390  
9 11     11   101 use Scalar::Util ();
  11         26  
  11         202  
10 11     11   5807 use Unicode::UTF8;
  11         6512  
  11         770  
11 11   50 11   80 use constant DEBUG => $ENV{MOJO_IRC_DEBUG} || 0;
  11         26  
  11         1314  
12 11   33 11   80 use constant DEFAULT_CERT => $ENV{MOJO_IRC_CERT_FILE} || catfile dirname(__FILE__), 'mojo-irc-client.crt';
  11         23  
  11         2105  
13 11   33 11   77 use constant DEFAULT_KEY => $ENV{MOJO_IRC_KEY_FILE} || catfile dirname(__FILE__), 'mojo-irc-client.key';
  11         21  
  11         23344  
14              
15             our $VERSION = '0.46';
16              
17             our %NUMERIC2NAME = (470 => 'ERR_LINKCHANNEL');
18              
19             my %CTCP_QUOTE = ("\012" => 'n', "\015" => 'r', "\0" => '0', "\cP" => "\cP");
20              
21             my @DEFAULT_EVENTS = qw(
22             irc_ping irc_nick irc_notice irc_rpl_welcome err_nicknameinuse
23             irc_rpl_isupport ctcp_ping ctcp_time ctcp_version
24             );
25              
26             has connect_timeout => sub { $ENV{MOJO_IRC_CONNECT_TIMEOUT} || 30 };
27             has ioloop => sub { Mojo::IOLoop->singleton };
28             has local_address => '';
29             has name => 'Mojo IRC';
30             has nick => sub { shift->_build_nick };
31             has parser => sub { Parse::IRC->new; };
32             has pass => '';
33             has real_host => '';
34              
35             has server_settings => sub {
36             return {chantypes => '#', prefix => '(ov)@+'};
37             };
38              
39             has tls => undef;
40             has user => sub { $ENV{USER} || getlogin || getpwuid($<) || 'anonymous' };
41              
42             sub new {
43 10     10 1 6606 my $self = shift->SUPER::new(@_);
44 10         246 $self->on(message => \&_legacy_dispatch_message);
45 10         144 return $self;
46             }
47              
48             sub server {
49 15     15 1 6193 my ($self, $server) = @_;
50 15   100     82 my $old = $self->{server} || '';
51              
52 15         59 Scalar::Util::weaken($self);
53 15 100       110 return $old unless defined $server;
54 4 100 100     32 return $self if $old and $old eq $server;
55 3         9 $self->{server} = $server;
56 3 50       14 return $self unless $self->{stream_id};
57             $self->disconnect(sub {
58 0     0   0 $self->connect(sub { });
59 0         0 });
60 0         0 $self;
61             }
62              
63             sub connect {
64 10     10 1 4308 my ($self, $cb) = @_;
65 10         54 my ($host, $port) = split /:/, $self->server;
66 10         28 my @extra;
67              
68 10 50       38 if (!$host) {
69 0     0   0 $self->ioloop->next_tick(sub { $self->$cb('server() is not set.') });
  0         0  
70 0         0 return $self;
71             }
72 10 50       46 if ($self->{stream_id}) {
73 0     0   0 $self->ioloop->next_tick(sub { $self->$cb('') });
  0         0  
74 0         0 return $self;
75             }
76              
77 10 100       57 if ($self->local_address) {
78 1         11 push @extra, local_address => $self->local_address;
79             }
80 10 50       103 if (my $tls = $self->tls) {
81 0         0 push @extra, tls => 1;
82 0 0       0 push @extra, tls_ca => $tls->{ca} if $tls->{ca}; # not sure why this should be supported, but adding it anyway
83 0   0     0 push @extra, tls_cert => $tls->{cert} || DEFAULT_CERT;
84 0   0     0 push @extra, tls_key => $tls->{key} || DEFAULT_KEY;
85 0 0       0 push @extra, tls_verify => 0x00 if $tls->{insecure}; # Mojolicious < 9.0
86 0 0       0 push @extra, tls_options => {SSL_verify_mode => 0x00} if $tls->{insecure}; # Mojolicious >= 9.0
87             }
88              
89 10   100     106 $port ||= 6667;
90 10         31 $self->{buffer} = '';
91 10   66     86 $self->{debug_key} ||= "$host:$port";
92 10         92 $self->register_default_event_handlers;
93              
94 10         33 Scalar::Util::weaken($self);
95             $self->{stream_id} = $self->ioloop->client(
96             address => $host,
97             port => $port,
98             timeout => $self->connect_timeout,
99             @extra,
100             sub {
101 9     9   15154 my ($loop, $err, $stream) = @_;
102              
103 9 100       40 if ($err) {
104 1         3 delete $self->{stream_id};
105 1         4 return $self->$cb($err);
106             }
107              
108 8         42 $stream->timeout(0);
109             $stream->on(
110             close => sub {
111 5 50       9654 $self or return;
112 5         73 warn "[$self->{debug_key}] : close\n" if DEBUG;
113 5         79 delete $self->{stream};
114 5         17 delete $self->{stream_id};
115 5         44 $self->emit('close');
116             }
117 8         268 );
118             $stream->on(
119             error => sub {
120 0 0       0 $self or return;
121 0 0       0 $self->ioloop or return;
122 0         0 $self->ioloop->remove(delete $self->{stream_id});
123 0         0 $self->emit(error => $_[1]);
124             }
125 8         83 );
126 8         76 $stream->on(read => sub { $self->_read($_[1]) });
  97         17204  
127              
128 8         54 $self->{stream} = $stream;
129             $self->ioloop->next_tick(sub {
130 8         4119 my @promises;
131 8 100       39 push @promises, $self->write_p(PASS => $self->pass) if length $self->pass;
132 8         85 push @promises, $self->write_p(NICK => $self->nick);
133 8         36 push @promises, $self->write_p(USER => $self->user, 8, '*', ':' . $self->name);
134 8         59 Mojo::Promise->all(@promises)->finally(sub { $self->$cb('') });
  6         2808  
135 8         44 });
136             }
137 10         51 );
138              
139 10         2479 return $self;
140             }
141              
142             sub ctcp {
143 3     3 1 8 my $self = shift;
144 3         9 local $_ = join ' ', @_;
145 3         20 s/([\012\015\0\cP])/\cP$CTCP_QUOTE{$1}/g;
146 3         8 s/\001/\\a/g;
147 3         15 ":\001${_}\001";
148             }
149              
150             sub disconnect {
151 3     3 1 1995 my ($self, $cb) = @_;
152              
153 3 50       11 if (my $tid = delete $self->{ping_tid}) {
154 0         0 $self->ioloop->remove($tid);
155             }
156              
157 3 100       12 if ($self->{stream}) {
    50          
158 2         9 Scalar::Util::weaken($self);
159             $self->{stream}->write(
160             "QUIT\r\n",
161             sub {
162 2     2   33 $self->{stream}->close;
163 2 100       13 $self->$cb if $cb;
164             }
165 2         42 );
166             }
167             elsif ($cb) {
168 0     0   0 $self->ioloop->next_tick(sub { $self->$cb });
  0         0  
169             }
170              
171 3         17 $self;
172             }
173              
174             sub register_default_event_handlers {
175 11     11 1 33 my $self = shift;
176              
177 11         36 for my $event (@DEFAULT_EVENTS) {
178 99 100       998 $self->on($event => $self->can($event)) unless $self->has_subscribers($event);
179             }
180              
181 11         112 return $self;
182             }
183              
184             sub track_any {
185 0     0 0 0 warn 'DEPRECATED! Just listen to $self->on(message => sub {}) instead.';
186 0         0 my $self = shift;
187 0 0 0     0 return $self->{track_any} || 0 unless @_;
188 0         0 $self->{track_any} = shift;
189 0         0 $self;
190             }
191              
192             sub write {
193 11     11   130 no warnings 'utf8';
  11         24  
  11         19783  
194 25 100   7 1 154 my $cb = ref $_[-1] eq 'CODE' ? pop : sub { };
        25      
195 25         52 my $self = shift;
196 25     0   248 my $buf = Unicode::UTF8::encode_utf8(join(' ', @_), sub { $_[0] });
  0         0  
197              
198 25         133 Scalar::Util::weaken($self);
199 25 50       99 if (ref $self->{stream}) {
200 25         36 warn "[$self->{debug_key}] <<< $buf\n" if DEBUG;
201 25     21   250 $self->{stream}->write("$buf\r\n", sub { $self->$cb(''); });
  21         4520  
202             }
203             else {
204 0     0   0 $self->ioloop->next_tick(sub { $self->$cb('Not connected.') });
  0         0  
205             }
206              
207 25         1075 $self;
208             }
209              
210             sub write_p {
211 17     17 1 181 my ($self, @args) = @_;
212 17         121 my $p = Mojo::Promise->new->ioloop($self->ioloop);
213 17 50   13   1017 $self->write(@args, sub { length $_[1] ? $p->reject($_[1]) : $p->resolve(1) });
  13         74  
214 17         47 return $p;
215             }
216              
217             sub ctcp_ping {
218 1     1 1 12 my ($self, $message) = @_;
219 1         4 my $ts = $message->{params}[1];
220 1         7 my $nick = IRC::Utils::parse_user($message->{prefix});
221              
222 1 50       17 return $self unless $ts;
223 1         6 return $self->write('NOTICE', $nick, $self->ctcp(PING => $ts));
224             }
225              
226             sub ctcp_time {
227 1     1 1 13 my ($self, $message) = @_;
228 1         4 my $nick = IRC::Utils::parse_user($message->{prefix});
229              
230 1         17 $self->write(NOTICE => $nick, $self->ctcp(TIME => scalar localtime));
231             }
232              
233             sub ctcp_version {
234 1     1 1 13 my ($self, $message) = @_;
235 1         4 my $nick = IRC::Utils::parse_user($message->{prefix});
236              
237 1         17 $self->write(NOTICE => $nick, $self->ctcp(VERSION => 'Mojo-IRC', $VERSION));
238             }
239              
240             sub irc_nick {
241 7     7 1 3529 my ($self, $message) = @_;
242 7   50     60 my $old_nick = ($message->{prefix} =~ /^[~&@%+]?(.*?)!/)[0] || '';
243              
244 7 100       26 if (lc $old_nick eq lc $self->nick) {
245 6         40 $self->nick($message->{params}[0]);
246             }
247             }
248              
249             sub irc_notice {
250 10     10 1 2510 my ($self, $message) = @_;
251              
252             # NOTICE AUTH :*** Ident broken or disabled, to continue to connect you must type /QUOTE PASS 21105
253 10 100       52 if ($message->{params}[0] =~ m!Ident broken.*QUOTE PASS (\S+)!) {
254 1         8 $self->write(QUOTE => PASS => $1);
255             }
256             }
257              
258             sub irc_ping {
259 1     1 1 550 my ($self, $message) = @_;
260 1         7 $self->write(PONG => $message->{params}[0]);
261             }
262              
263             sub irc_rpl_isupport {
264 2     2 1 22 my ($self, $message) = @_;
265 2         4 my $params = $message->{params};
266 2         6 my $server_settings = $self->server_settings;
267 2         7 my %got;
268              
269 2         7 for my $i (1 .. @$params - 1) {
270 24 100       85 next unless $params->[$i] =~ /([A-Z]+)=?(\S*)/;
271 22         58 my ($k, $v) = (lc $1, $2);
272 22         61 $got{$k} = 1;
273 22   100     68 $server_settings->{$k} = $v || 1;
274             }
275             }
276              
277             sub irc_rpl_welcome {
278 2     2 1 556 my ($self, $message) = @_;
279 2         14 $self->nick($message->{params}[0]);
280              
281 2         23 Scalar::Util::weaken($self);
282 2         16 $self->real_host($message->{prefix});
283             $self->{ping_tid} ||= $self->ioloop->recurring(
284             $self->{ping_pong_interval} || 60, # $self->{ping_pong_interval} is EXPERIMENTAL
285             sub {
286 1     1   2338 $self->write(PING => $self->real_host);
287             }
288 2   50     29 );
      33        
289             }
290              
291             sub err_nicknameinuse {
292 1     1 1 576 my ($self, $message) = @_;
293 1         4 my $nick = $message->{params}[1];
294              
295 1         7 $self->nick($nick . '_');
296 1     1   10 $self->write(NICK => $self->nick, sub { });
297             }
298              
299             sub DESTROY {
300 7     7   16086 my $self = shift;
301 7 50       45 my $ioloop = $self->ioloop or return;
302 7         105 my $tid = $self->{ping_tid};
303 7         20 my $sid = $self->{stream_id};
304              
305 7 100       50 $ioloop->remove($sid) if $sid;
306 7 100       1530 $ioloop->remove($tid) if $tid;
307             }
308              
309             sub _build_nick {
310 3     3   17 my $nick = shift->user;
311 3         30 $nick =~ s![^a-z_]!_!g;
312 3         25 $nick;
313             }
314              
315             sub _dispatch_message {
316 53     53   93 my ($self, $msg) = @_;
317 53 50       158 $self->emit(irc_any => $msg) if $self->{track_any}; # will be deprecated
318 53         162 $self->emit(message => $msg);
319             }
320              
321             sub _legacy_dispatch_message {
322 53     53   505 my ($self, $msg) = @_;
323 53         82 my $event = $msg->{event};
324              
325 53 100       216 $event = "irc_$event" unless $event =~ /^(ctcp|err)_/;
326 53         82 warn "[$self->{debug_key}] === $event\n" if DEBUG == 2;
327 53         120 $self->emit($event => $msg);
328             }
329              
330             # Can be used in unittest to mock input data:
331             # $irc->_read($bytes);
332             sub _read {
333 97     97   165 my $self = shift;
334              
335 11     11   122 no warnings 'utf8';
  11         27  
  11         4570  
336 97     0   882 $self->{buffer} .= Unicode::UTF8::decode_utf8($_[0], sub { $_[0] });
  0         0  
337              
338             CHUNK:
339 97         1322 while ($self->{buffer} =~ s/^([^\015\012]+)[\015\012]//m) {
340 53         169 warn "[$self->{debug_key}] >>> $1\n" if DEBUG;
341 53         159 my $msg = $self->parser->parse($1);
342 53 50       3322 my $cmd = $msg->{command} or next CHUNK;
343 53 100 33     330 $msg->{command} = $NUMERIC2NAME{$cmd} || IRC::Utils::numeric_to_name($cmd) || $cmd if $cmd =~ /^\d+$/;
344 53         394 $msg->{event} = lc $msg->{command};
345 53         137 $self->_dispatch_message($msg);
346             }
347             }
348              
349             1;
350              
351             =encoding utf8
352              
353             =head1 NAME
354              
355             Mojo::IRC - IRC Client for the Mojo IOLoop
356              
357             =head1 VERSION
358              
359             0.46
360              
361             =head1 SYNOPSIS
362              
363             my $irc = Mojo::IRC->new(
364             nick => 'test123',
365             user => 'my name',
366             server => 'irc.perl.org:6667',
367             );
368              
369             $irc->on(irc_join => sub {
370             my($self, $message) = @_;
371             warn "yay! i joined $message->{params}[0]";
372             });
373              
374             $irc->on(irc_privmsg => sub {
375             my($self, $message) = @_;
376             say $message->{prefix}, " said: ", $message->{params}[1];
377             });
378              
379             $irc->connect(sub {
380             my($irc, $err) = @_;
381             return warn $err if $err;
382             $irc->write(join => '#mojo');
383             });
384              
385             Mojo::IOLoop->start;
386              
387             =head1 DESCRIPTION
388              
389             L is a non-blocking IRC client using L from the
390             wonderful L framework.
391              
392             It features IPv6 and TLS, with additional optional modules:
393             L and L.
394              
395             By default this module will only emit standard IRC events, but by
396             settings L to a custom object it will also emit CTCP events.
397             Example:
398              
399             my $irc = Mojo::IRC->new;
400             $irc->parser(Parse::IRC->new(ctcp => 1);
401             $irc->on(ctcp_action => sub {
402             # ...
403             });
404              
405             It will also set up some default events: L, L,
406             and L.
407              
408             This class inherits from L.
409              
410             =head1 TESTING
411              
412             The module L is useful if you want to write tests without
413             having a running IRC server.
414              
415             L (from v0.20) is now DEPRECATED in favor of
416             L.
417              
418             =head1 EVENTS
419              
420             =head2 close
421              
422             $self->on(close => sub { my ($self) = @_; });
423              
424             Emitted once the connection to the server closes.
425              
426             =head2 error
427              
428             $self->on(error => sub { my ($self, $err) = @_; });
429              
430             Emitted once the stream emits an error.
431              
432             =head2 message
433              
434             $self->on(message => sub { my ($self, $msg) = @_; });
435              
436             Emitted when a new IRC message arrives. Will dispatch to a default handler,
437             which will again emit L L and
438             L below.
439              
440             Here is an example C<$msg>:
441              
442             {
443             command => "PRIVMSG",
444             event => "privmsg",
445             params => ["#convos", "hey!"],
446             prefix => "jan_henning",
447             raw_line => ":jan_henning PRIVMSG #convos :hey",
448             }
449              
450             =head2 err_event_name
451              
452             Events that start with "err_" are emitted when there is an IRC response that
453             indicates an error. See L for sample events.
454              
455             =head2 ctcp_event_name
456              
457             Events that start with "ctcp_" are emitted if the L can understand
458             CTCP messages, and there is a CTCP response.
459              
460             $self->parser(Parse::IRC->new(ctcp => 1);
461              
462             See L for sample events.
463              
464             =head2 irc_event_name
465              
466             Events that start with "irc_" are emitted when there is a normal IRC response.
467             See L for sample events.
468              
469             =head1 ATTRIBUTES
470              
471             =head2 connect_timeout
472              
473             $int = $self->connect_timeout;
474             $self = $self->connect_timeout(60);
475              
476             Maximum amount of time in seconds establishing a connection may take before
477             getting canceled, defaults to the value of the C
478             environment variable or 30.
479              
480             =head2 ioloop
481              
482             Holds an instance of L.
483              
484             =head2 local_address
485              
486             $str = $self->local_address;
487             $self = $self->local_address("10.20.30.40");
488              
489             Local address to bind to. See L.
490              
491             =head2 name
492              
493             The name of this IRC client. Defaults to "Mojo IRC".
494              
495             =head2 nick
496              
497             IRC nick name accessor. Default to L.
498              
499             =head2 parser
500              
501             $self = $self->parser($obj);
502             $self = $self->parser(Parse::IRC->new(ctcp => 1));
503             $obj = $self->parser;
504              
505             Holds a L object by default.
506              
507             =head2 pass
508              
509             Password for authentication
510              
511             =head2 real_host
512              
513             Will be set by L. Holds the actual hostname of the IRC
514             server that we are connected to.
515              
516             =head2 server
517              
518             Server name and, optionally, a port to connect to. Changing this while
519             connected to the IRC server will issue a reconnect.
520              
521             =head2 server_settings
522              
523             $hash = $self->server_settings;
524              
525             Holds information about the server. See
526             L for
527             example data structure.
528              
529             Note that this attribute is EXPERIMENTAL and the structure of the values it
530             holds.
531              
532             =head2 user
533              
534             IRC username. Defaults to current logged in user or falls back to "anonymous".
535              
536             =head2 tls
537              
538             $self->tls(undef) # disable (default)
539             $self->tls({}) # enable
540              
541             Default is "undef" which disables TLS. Setting this to an empty hash will
542             enable TLS and this module will load in default certs. It is also possible
543             to set custom cert/key:
544              
545             $self->tls({ cert => "/path/to/client.crt", key => ... })
546              
547             This can be generated using
548              
549             # certtool --generate-privkey --outfile client.key
550             # certtool --generate-self-signed --load-privkey client.key --outfile client.crt
551              
552             To disable the verification of server certificates, the "insecure" option
553             can be set:
554              
555             $self->tls({insecure => 1});
556              
557             =head1 METHODS
558              
559             =head2 connect
560              
561             $self = $self->connect(\&callback);
562              
563             Will log in to the IRC L and call C<&callback>. The
564             C<&callback> will be called once connected or if connect fails. The second
565             argument will be an error message or empty string on success.
566              
567             =head2 ctcp
568              
569             $str = $self->ctcp(@str);
570              
571             This message will quote CTCP messages. Example:
572              
573             $self->write(PRIVMSG => nickname => $self->ctcp(TIME => time));
574              
575             The code above will write this message to IRC server:
576              
577             PRIVMSG nickname :\001TIME 1393006707\001
578              
579             =head2 disconnect
580              
581             $self->disconnect(\&callback);
582              
583             Will disconnect form the server and run the callback once it is done.
584              
585             =head2 new
586              
587             $self = Mojo::IRC->new(%attrs);
588              
589             Object constructor.
590              
591             =head2 register_default_event_handlers
592              
593             $self->register_default_event_handlers;
594              
595             This method sets up the default L unless someone has
596             already subscribed to the event.
597              
598             =head2 write
599              
600             $self->write(@str, \&callback);
601              
602             This method writes a message to the IRC server. C<@str> will be concatenated
603             with " " and "\r\n" will be appended. C<&callback> is called once the message is
604             delivered over the stream. The second argument to the callback will be
605             an error message: Empty string on success and a description on error.
606              
607             =head2 write_p
608              
609             $promise = $self->write_p(@str);
610              
611             Like L, but returns a L instead of taking a callback.
612             The promise will be resolved on success, or rejected with the error message on
613             error.
614              
615             =head1 DEFAULT EVENT HANDLERS
616              
617             =head2 ctcp_ping
618              
619             Will respond to the sender with the difference in time.
620              
621             Ping reply from $sender: 0.53 second(s)
622              
623             =head2 ctcp_time
624              
625             Will respond to the sender with the current localtime. Example:
626              
627             TIME Fri Feb 21 18:56:50 2014
628              
629             NOTE! The localtime format may change.
630              
631             =head2 ctcp_version
632              
633             Will respond to the sender with:
634              
635             VERSION Mojo-IRC $VERSION
636              
637             NOTE! Additional information may be added later on.
638              
639             =head2 irc_nick
640              
641             Used to update the L attribute when the nick has changed.
642              
643             =head2 irc_notice
644              
645             Responds to the server with "QUOTE PASS ..." if the notice contains "Ident
646             broken...QUOTE PASS...".
647              
648             =head2 irc_ping
649              
650             Responds to the server with "PONG ...".
651              
652             =head2 irc_rpl_isupport
653              
654             Used to populate L with information about the server.
655              
656             =head2 irc_rpl_welcome
657              
658             Used to get the hostname of the server. Will also set up automatic PING
659             requests to prevent timeout and update the L attribute.
660              
661             =head2 err_nicknameinuse
662              
663             This handler will add "_" to the failed nick before trying to register again.
664              
665             =head1 COPYRIGHT
666              
667             This program is free software, you can redistribute it and/or modify it under
668             the terms of the Artistic License version 2.0.
669              
670             =head1 AUTHOR
671              
672             Marcus Ramberg - C
673              
674             Jan Henning Thorsen - C
675              
676             =cut