File Coverage

blib/lib/Alice/Role/IRCEvents.pm
Criterion Covered Total %
statement 47 87 54.0
branch 9 30 30.0
condition 1 6 16.6
subroutine 10 15 66.6
pod 0 6 0.0
total 67 144 46.5


line stmt bran cond sub pod time code
1             package Alice::Role::IRCEvents;
2              
3 2     2   1391 use AnyEvent;
  2         7  
  2         52  
4 2     2   10 use Any::Moose 'Role';
  2         6  
  2         22  
5 2     2   1056 use IRC::Formatting::HTML qw/irc_to_html/;
  2         5  
  2         143  
6 2     2   13 use AnyEvent::IRC::Util qw/split_prefix/;
  2         5  
  2         104  
7 2     2   12 use List::Util qw/min/;
  2         5  
  2         173  
8 2     2   13 use Try::Tiny;
  2         5  
  2         127  
9 2     2   10 use Class::Throwable qw/InvalidNetwork DisconnectError ConnectError/;
  2         4  
  2         18  
10              
11             our %EVENTS;
12              
13             sub build_events {
14 1     1 0 3 my ($self, $irc) = @_;
15              
16             return +{
17 36         46 map {
18 1         14 my $event = $_;
19             $event => sub {
20 0     0   0 my @args = @_; # we don't need the client
21 0         0 shift @args;
22 0         0 AE::log trace => "$event event for " . $irc->name;
23             try {
24 0         0 $EVENTS{$event}->($self, $irc, @args);
25             }
26             catch {
27 0         0 AE::log debug => "Error in $event: $_";
28 0         0 $self->send_info("Please report this bug! $_");
29             }
30 0         0 }
31 36         175 } keys %EVENTS
32             }
33             }
34              
35             sub irc_event {
36 78     78 0 108 my ($name, $code) = @_;
37              
38 78 50       166 if ($code) {
39 78 100       126 if (ref $name eq 'ARRAY') {
40 4         16 irc_event($_, $code) for @$name;
41 4         10 return;
42             }
43              
44 74 100       258 $name = "irc_$name" if $name =~ /^\d+$/;
45             }
46              
47 74         237 $EVENTS{$name} = $code;
48             }
49              
50             irc_event connect => sub {
51             my ($self, $irc, $err) = @_;
52             $irc->is_connecting(0);
53              
54             if ($irc->cl->{socket}) {
55             $irc->cl->{socket}->rbuf_max(1024 * 10); # 10K max read buffer
56             }
57              
58             if (defined $err) {
59             $self->send_info($irc->name, "connect error: $err");
60             $self->reconnect_irc($irc->name);
61             return;
62             }
63              
64             $self->send_info($irc->name, "connected");
65             $irc->connect_time(time);
66              
67             $self->broadcast({
68             type => "action",
69             event => "connect",
70             network => $irc->name,
71             });
72              
73             my $config = $self->config->servers->{$irc->name};
74              
75             $irc->cl->register(
76             $config->{nick}, $config->{username}, $config->{ircname}, $config->{password}
77             );
78             };
79              
80             irc_event registered => sub {
81             my ($self, $irc) = @_;
82             my $config = $self->config->servers->{$irc->name};
83              
84             $irc->reset_reconnect_count;
85             $irc->cl->{connected} = 1; # AE::IRC seems broken here...
86              
87             my @commands = ();
88              
89             push @commands, map {
90             my $command = $_;
91             sub {
92             $self->send_info($irc->name, "sending $command");
93             $irc->send_raw($command);
94             }
95             } @{$config->{on_connect}};
96              
97             push @commands, map {
98             my $channel = $_;
99             sub {
100             $self->send_info($irc->name, "joining $channel");
101             $irc->send_srv("JOIN", split /\s+/, $channel);
102             }
103             } @{$config->{channels}};
104            
105             my $t; $t = AE::timer 1, 0.5, sub {
106             if (my $command = shift @commands) {
107             $command->();
108             }
109             else {
110             undef $t;
111             }
112             };
113              
114             my $name = $irc->name;
115             $irc->cl->enable_ping(300 => sub { $self->reconnect_irc($name) });
116             };
117              
118             irc_event disconnect => sub {
119             my ($self, $irc, $reason) = @_;
120              
121             my @windows = grep {$_->network eq $irc->name} $self->windows;
122             $self->broadcast({
123             type => "action",
124             event => "disconnect",
125             network => $irc->name,
126             windows => [map {$_->serialized} @windows],
127             });
128             $self->remove_window($_) for map {$_->id} @windows;
129              
130             $reason = "" unless $reason;
131             return if $reason eq "reconnect requested.";
132             $self->send_info($irc->name, "disconnected: $reason");
133            
134             # TODO - Object::Event bug that prevents object from getting destroyed
135             delete $irc->cl->{change_nick_cb_guard} if $irc->cl;
136              
137             $irc->cl(undef);
138              
139             $self->reconnect_irc($irc->name, 0) unless $irc->disabled;
140              
141             if ($irc->removed) {
142             $self->remove_irc($irc->name);
143             }
144             };
145              
146             irc_event publicmsg => sub {
147             my ($self, $irc, $channel, $msg) = @_;
148              
149             if (my $window = $self->find_window($channel, $irc)) {
150             my ($nick) = split_prefix($msg->{prefix});
151             my $text = $msg->{params}[1];
152              
153             return if $self->is_ignore(msg => $nick);
154              
155             $self->send_message($window, $nick, $text);
156             }
157             };
158              
159             irc_event privatemsg => sub {
160             my ($self, $irc, $nick, $msg) = @_;
161              
162             my $text = $msg->{params}[1];
163             my ($from) = split_prefix($msg->{prefix});
164              
165             if ($msg->{command} eq "PRIVMSG") {
166             return if $self->is_ignore(msg => $from);
167              
168             my $window = $self->find_or_create_window($from, $irc);
169             $self->send_message($window, $from, $text);
170             $irc->send_srv(WHO => $from) unless $irc->nick_avatar($from);
171             }
172             elsif ($msg->{command} eq "NOTICE") {
173             $self->send_info($from, $text);
174             }
175             };
176              
177             irc_event ctcp_action => sub {
178             my ($self, $irc, $nick, $channel, $msg, $type) = @_;
179             return unless $msg;
180             return if $self->is_ignore(msg => $nick);
181              
182             my $dest = ($channel eq $irc->nick ? $nick : $channel);
183              
184             if (my $window = $self->find_or_create_window($dest, $irc)) {
185             my $text = "\x{2022} $msg";
186             $self->send_message($window, $nick, $text);
187             }
188             };
189              
190             irc_event nick_change => sub {
191             my ($self, $irc, $old_nick, $new_nick, $is_self) = @_;
192              
193             my @channels = $irc->nick_channels($new_nick);
194              
195             $self->broadcast(
196             grep {$_}
197             map {
198             if (my $window = $self->find_window($_, $irc)) {
199             $window->nicks_action($irc->channel_nicks($window->title)),
200             $self->is_ignore(nick => $_) ? ()
201             : $window->format_event("nick", $old_nick, $new_nick)
202             }
203             } @channels
204             );
205              
206             if ($irc->avatars->{$old_nick}) {
207             $irc->avatars->{$new_nick} = delete $irc->avatars->{$old_nick};
208             }
209             };
210              
211             irc_event 301 => sub {
212             my ($self, $irc, $msg) = @_;
213              
214             my (undef, $from, $awaymsg) = @{$msg->{params}};
215              
216             if (my $window = $self->find_window($from, $irc)) {
217             $awaymsg = "$from is away ($awaymsg)";
218             $self->announce($awaymsg);
219             }
220             };
221              
222             irc_event 319 => sub {
223             my ($self, $irc, $msg) = @_;
224              
225             # ignore the first param if it is our own nick, some servers include it
226             shift @{$msg->{params}} if $msg->{params}[0] eq $irc->nick;
227              
228             my ($nick, $channels) = @{$msg->{params}};
229              
230             if (my $whois = $irc->whois->{lc $nick}) {
231             $whois->{info} .= "\nchannels: " .
232             join " ", map {
233             my $modes = $irc->cl->nick_modes($nick, $_);
234             $irc->prefix_from_modes($nick, $modes) . $_;
235             } split /\s+/, $channels;
236             }
237             };
238              
239             irc_event 352 => sub {
240             my ($self, $irc, $msg) = @_;
241              
242             # ignore the first param if it is our own nick, some servers include it
243             shift @{$msg->{params}} if $msg->{params}[0] eq $irc->nick;
244            
245             my (undef, undef, undef, undef, $nick, undef, @real) = @{$msg->{params}};
246             my $real = join "", @real;
247             $real =~ s/^[0-9*] //;
248             if (my $avatar = $irc->realname_avatar($real)) {
249             $irc->avatars->{$nick} = $avatar;
250             }
251             };
252              
253             irc_event 311 => sub {
254             my ($self, $irc, $msg) = @_;
255              
256             # ignore the first param if it is our own nick, some servers include it
257             shift @{$msg->{params}} if $msg->{params}[0] eq $irc->nick;
258              
259             # hector adds an extra nick param or something
260             shift @{$msg->{params}} if scalar @{$msg->{params}} > 5;
261              
262             my ($nick, $user, $address, undef, $real) = @{$msg->{params}};
263              
264             if (my $avatar = $irc->realname_avatar($real)) {
265             $irc->avatars->{$nick} = $avatar;
266             }
267              
268             if (my $whois = $irc->whois->{lc $nick}) {
269             $whois->{info} .= "nick: $nick"
270             . "\nuser: $user"
271             . "\nreal: $real"
272             . "\nIP: $address";
273             }
274             };
275              
276             irc_event 312 => sub {
277             my ($self, $irc, $msg) = @_;
278              
279             # ignore the first param if it is our own nick, some servers include it
280             shift @{$msg->{params}} if $msg->{params}[0] eq $irc->nick;
281              
282             my ($nick, $server) = @{$msg->{params}};
283              
284             if (my $whois = $irc->whois->{lc $nick}) {
285             $whois->{info} .= "\nserver: $server";
286             }
287             };
288              
289             irc_event 318 => sub {
290             my ($self, $irc, $msg) = @_;
291              
292             # ignore the first param if it is our own nick, some servers include it
293             shift @{$msg->{params}} if $msg->{params}[0] eq $irc->nick;
294              
295             my $nick = $msg->{params}[0];
296              
297             if (my $whois = $irc->whois->{lc $nick}) {
298             $whois->{cb}->($whois->{info});
299             delete $irc->whois->{lc $nick};
300             }
301             };
302              
303             irc_event 366 => sub {
304             my ($self, $irc, $msg) = @_;
305             my $channel = $msg->{params}[1];
306             if (my $window = $self->find_window($channel, $irc)) {
307             $self->broadcast(
308             $window->nicks_action($irc->channel_nicks($channel))
309             );
310             }
311             };
312              
313             irc_event 401 => sub {
314             my ($self, $irc, $msg) = @_;
315              
316             my $nick = $msg->{params}[1];
317              
318             if (my $window = $self->find_window($nick, $irc)) {
319             $self->announce("No such nick.");
320             }
321            
322             if ($irc->whois->{$nick}) {
323             $self->whois->{$nick}{cb}->();
324             delete $self->whois->{$nick};
325             }
326             };
327              
328             irc_event join => sub {
329             my ($self, $irc, $nick, $channel, $is_self) = @_;
330              
331             if ($is_self) {
332             my $window = $self->find_or_create_window($channel, $irc);
333             $self->broadcast(
334             $window->format_event("joined", "you"),
335             $window->join_action,
336             $window->nicks_action($irc->channel_nicks($channel)),
337             );
338             $irc->send_srv("WHO" => $channel) if $irc->cl->isupport("UHNAMES");
339             }
340             };
341              
342             irc_event channel_add => sub {
343             my ($self, $irc, $msg, $channel, @nicks) = @_;
344              
345             if (my $window = $self->find_window($channel, $irc)) {
346             $self->broadcast(
347             $window->nicks_action($irc->channel_nicks($channel))
348             );
349              
350             if ($msg->{command} eq "JOIN" and !$self->is_ignore("join" => $channel)) {
351             $self->broadcast(
352             map {$window->format_event("joined", $_)} @nicks
353             );
354             }
355             }
356             };
357              
358             irc_event part => sub {
359             my ($self, $irc, $nick, $channel, $is_self, $msg) = @_;
360              
361             if ($is_self and my $window = $self->find_window($channel, $irc)) {
362             $self->send_info($irc->name, "leaving $channel");
363             $self->close_window($window);
364             }
365             };
366              
367             irc_event channel_remove => sub {
368             my ($self, $irc, $msg, $channel, @nicks) = @_;
369              
370             if (my $window = $self->find_window($channel, $irc)) {
371             $self->broadcast(
372             $window->nicks_action($irc->channel_nicks($channel))
373             );
374              
375             unless ($self->is_ignore(part => $channel)) {
376             my $reason = "";
377              
378             if ($msg and $msg->{command} eq "QUIT") {
379             $reason = $msg->{params}[-1] || "Quit";
380             }
381              
382             $self->broadcast(
383             map {$window->format_event(left => $_, $reason)} @nicks
384             );
385             }
386             }
387             };
388              
389             irc_event channel_topic => sub {
390             my ($self, $irc, $channel, $topic, $nick) = @_;
391             if (my $window = $self->find_window($channel, $irc)) {
392             $topic = irc_to_html($topic, classes => 1, invert => "italic");
393             $window->topic({string => $topic, author => $nick, time => time});
394             $self->broadcast($window->format_event("topic", $nick, $topic));
395             }
396             };
397              
398             irc_event irc_invite => sub {
399             my ($self, $irc, $msg) = @_;
400              
401             my (undef, $channel) = @{$msg->{params}};
402             my ($from) = split_prefix($msg->{prefix});
403              
404             my $message = "$from has invited you to $channel on ".$irc->name;
405             $self->announce($message);
406             };
407              
408             irc_event 464 => sub{
409             my ($self, $irc, $msg) = @_;
410             $self->disconnect_irc($irc->name, "bad USER/PASS")
411             };
412              
413             irc_event [qw/001 305 306 401 471 473 474 475 477 485 432 433/] => sub {
414             my ($self, $irc, $msg) = @_;
415             $self->send_info($irc->name, $msg->{params}[-1]);
416             };
417              
418             irc_event [qw/372 377 378/] => sub {
419             my ($self, $irc, $msg) = @_;
420             $self->send_info($irc->name, $msg->{params}[-1], mono => 1);
421             };
422              
423             sub reconnect_irc {
424 0     0 0 0 my ($self, $name, $time) = @_;
425 0         0 my $irc = $self->get_irc($name);
426 0 0       0 throw InvalidNetwork "$name isn't one of your networks" unless $irc;
427              
428 0         0 my $interval = time - $irc->connect_time;
429              
430 0 0       0 if ($interval < 15) {
431 0         0 $time = 15 - $interval;
432 0         0 $self->send_info($irc->name, "last attempt was within 15 seconds, delaying $time seconds")
433             }
434              
435 0 0       0 if (!defined $time) {
436             # increase timer by 15 seconds each time, until it hits 5 minutes
437 0         0 $time = min 60 * 5, 15 * $irc->reconnect_count;
438             }
439              
440 0         0 $self->send_info($irc->name, "reconnecting in $time seconds");
441 0     0   0 $irc->reconnect_timer(AE::timer $time, 0, sub {$self->connect_irc($name)});
  0         0  
442             }
443              
444             sub disconnect_irc {
445 0     0 0 0 my ($self, $name, $msg) = @_;
446 0         0 my $irc = $self->get_irc($name);
447 0 0       0 throw InvalidNetwork "$name isn't one of your networks" unless $irc;
448              
449 0 0       0 if ($irc->reconnect_timer) {
450 0         0 $self->cancel_reconnect($name);
451 0         0 return;
452             }
453              
454 0 0       0 throw DisconnectError "$name is already disconnected" if $irc->is_disconnected;
455              
456 0 0       0 $self->send_info($irc->name, "disconnecting: $msg") if $msg;
457 0         0 $irc->is_connecting(0);
458 0         0 $irc->disabled(1);
459 0   0     0 $msg ||= $self->config->quitmsg;
460 0         0 $irc->cl->disconnect($msg);
461             }
462              
463             sub cancel_reconnect {
464 0     0 0 0 my ($self, $name) = @_;
465 0         0 my $irc = $self->get_irc($name);
466 0 0       0 throw InvalidNetwork "$name isn't one of your networks" unless $irc;
467              
468 0         0 $self->send_info($irc->name, "canceled reconnect");
469 0         0 $self->broadcast({
470             type => "action",
471             event => "disconnect",
472             network => $irc->name,
473             windows => [], #shouldn't be any windows if we're not connected.
474             });
475 0         0 $irc->reconnect_timer(undef);
476 0         0 $irc->reset_reconnect_count;
477             }
478              
479             sub connect_irc {
480 1     1 0 3 my ($self, $name) = @_;
481 1         5 my $irc = $self->get_irc($name);
482              
483 1 50       7 throw InvalidNetwork "$name isn't one of your networks" unless $irc;
484 1 50       6 throw ConnectError "$name is already connected" if $irc->is_connected;
485 1 50       7 throw ConnectError "$name is already connecting" if $irc->is_connecting;
486              
487 1         5 $irc->reconnect_timer(undef);
488 1         7 my $config = $self->config->servers->{$irc->name};
489            
490             # some people don't set these, wtf
491 1 50 33     10 if (!$config->{host} or !$config->{port}) {
492 0         0 $self->send_info($irc->name, "can't connect: missing either host or port");
493 0         0 return;
494             }
495              
496 1         6 my $events = $self->build_events($irc);
497 1         12 $irc->new_client($events, $config);
498 1         5 $irc->disabled(0);
499 1         5 $irc->increase_reconnect_count;
500            
501 1         14 $self->send_info($irc->name, "connecting (attempt " . $irc->reconnect_count .")");
502            
503 1         32 $irc->is_connecting(1);
504 1         46 $irc->cl->connect($config->{host}, $config->{port});
505             }
506              
507             1;