File Coverage

blib/lib/Alice.pm
Criterion Covered Total %
statement 139 316 43.9
branch 14 80 17.5
condition 3 28 10.7
subroutine 48 85 56.4
pod 1 52 1.9
total 205 561 36.5


line stmt bran cond sub pod time code
1             package Alice;
2              
3 2     2   34209 use AnyEvent;
  2         11343  
  2         89  
4 2     2   2012 use AnyEvent::Strict;
  2         33960  
  2         68  
5 2     2   1940 use AnyEvent::Log;
  2         8834  
  2         66  
6              
7 2     2   1802 use Any::Moose;
  2         82783  
  2         15  
8 2     2   3551 use Text::MicroTemplate::File;
  2         15483  
  2         117  
9 2     2   20 use Digest::MD5 qw/md5_hex/;
  2         4  
  2         370  
10 2     2   14 use List::Util qw/first/;
  2         4  
  2         194  
11 2     2   2538 use List::MoreUtils qw/any uniq/;
  2         3233  
  2         398  
12 2     2   2689 use AnyEvent::IRC::Util qw/filter_colors/;
  2         53960  
  2         227  
13 2     2   2063 use IRC::Formatting::HTML qw/html_to_irc/;
  2         29620  
  2         147  
14 2     2   21 use Encode;
  2         4  
  2         171  
15              
16 2     2   1119 use Alice::Window;
  2         9  
  2         89  
17 2     2   6795 use Alice::InfoWindow;
  2         6  
  2         63  
18 2     2   1117 use Alice::HTTP::Server;
  2         7  
  2         64  
19 2     2   1287 use Alice::IRC;
  2         9  
  2         99  
20 2     2   1270 use Alice::Config;
  2         17  
  2         135  
21 2     2   1342 use Alice::Tabset;
  2         7  
  2         68  
22 2     2   922 use Alice::Request;
  2         7  
  2         55  
23 2     2   15 use Alice::MessageBuffer;
  2         3  
  2         43  
24 2     2   999 use Alice::MessageStore;
  2         9  
  2         9499  
25              
26             our $VERSION = '0.20';
27              
28             with 'Alice::Role::Commands';
29             with 'Alice::Role::IRCEvents';
30              
31             has config => (
32             is => 'rw',
33             isa => 'Alice::Config',
34             );
35              
36             has _ircs => (
37             is => 'rw',
38             isa => 'ArrayRef',
39             default => sub {[]},
40             );
41              
42 4     4 0 15 sub ircs {@{$_[0]->_ircs}}
  4         74  
43 1     1 0 3 sub add_irc {push @{$_[0]->_ircs}, $_[1]}
  1         7  
44 1     1 0 9 sub has_irc {$_[0]->get_irc($_[1])}
45 3     3 0 20 sub get_irc {first {$_->name eq $_[1]} $_[0]->ircs}
  3     3   18  
46 0     0 0 0 sub remove_irc {$_[0]->_ircs([ grep { $_->name ne $_[1] } $_[0]->ircs])}
  0         0  
47 0     0 0 0 sub irc_names {map {$_->name} $_[0]->ircs}
  0         0  
48 0     0 0 0 sub connected_ircs {grep {$_->is_connected} $_[0]->ircs}
  0         0  
49              
50             has streams => (
51             is => 'rw',
52             isa => 'ArrayRef',
53             default => sub {[]},
54             );
55              
56 0     0 0 0 sub add_stream {unshift @{shift->streams}, @_}
  0         0  
57 2     2 0 3 sub no_streams {@{$_[0]->streams} == 0}
  2         62  
58 0     0 0 0 sub stream_count {scalar @{$_[0]->streams}}
  0         0  
59              
60             has message_store => (
61             is => 'ro',
62             lazy => 1,
63             default => sub {
64             my $self = shift;
65             my $buffer = $self->config->path."/buffer.db";
66             if (! -e $buffer) {
67             require File::Copy;
68             File::Copy::copy($self->config->assetdir."/buffer.db", $buffer);
69             }
70             Alice::MessageStore->new(dsn => ["dbi:SQLite:dbname=$buffer", "", ""]);
71             }
72             );
73              
74             has _windows => (
75             is => 'rw',
76             isa => 'ArrayRef',
77             default => sub {[]},
78             );
79              
80 10     10 0 14 sub windows {@{$_[0]->_windows}}
  10         57  
81 3     3 0 13 sub add_window {push @{$_[0]->_windows}, $_[1]}
  3         47  
82 2     2 0 24 sub has_window {$_[0]->get_window($_[1])}
83 14     14 0 96 sub get_window {first {$_->id eq $_[1]} $_[0]->windows}
  7     7   52  
84 2     2 0 16 sub remove_window {$_[0]->_windows([grep {$_->id ne $_[1]} $_[0]->windows])}
  5         27  
85 0     0 0 0 sub window_ids {map {$_->id} $_[0]->windows}
  0         0  
86              
87             has 'template' => (
88             is => 'ro',
89             isa => 'Text::MicroTemplate::File',
90             lazy => 1,
91             default => sub {
92             my $self = shift;
93             Text::MicroTemplate::File->new(
94             include_path => $self->config->assetdir . '/templates',
95             cache => 2,
96             );
97             },
98             );
99              
100             has 'info_window' => (
101             is => 'ro',
102             isa => 'Alice::InfoWindow',
103             lazy => 1,
104             default => sub {
105             my $self = shift;
106             my $id = $self->_build_window_id("info", "info");
107             my $info = Alice::InfoWindow->new(
108             id => $id,
109             buffer => $self->_build_window_buffer($id),
110             render => sub { $self->render(@_) }
111             );
112             $self->add_window($info);
113             return $info;
114             }
115             );
116              
117             has 'user' => (
118             is => 'ro',
119             default => $ENV{USER}
120             );
121              
122             sub BUILDARGS {
123 1     1 1 1124 my ($class, %options) = @_;
124              
125 1         3 my $self = {};
126              
127 1         4 for (qw/template user message_store/) {
128 3 50       14 if (exists $options{$_}) {
129 0         0 $self->{$_} = $options{$_};
130 0         0 delete $options{$_};
131             }
132             }
133              
134 1     1   6 $self->{config} = Alice::Config->new(
135             %options,
136             callback => sub {$self->{config}->merge(\%options)}
137 1         21 );
138              
139 1         25 return $self;
140             }
141              
142             sub run {
143 0     0 0 0 my $self = shift;
144              
145             # wait for config to finish loading
146 0         0 my $w; $w = AE::idle sub {
147 0 0   0   0 return unless $self->config->{loaded};
148 0         0 undef $w;
149 0         0 $self->init;
150 0         0 };
151             }
152              
153             sub init {
154 0     0 0 0 my $self = shift;
155 0         0 $self->info_window;
156 0         0 $self->template;
157              
158 0         0 $self->add_irc_server($_, $self->config->servers->{$_})
159 0         0 for keys %{$self->config->servers};
160             }
161              
162             sub init_shutdown {
163 0     0 0 0 my ($self, $cb, $msg) = @_;
164              
165 0         0 $self->alert("Alice server is shutting down");
166 0         0 $self->disconnect_irc($_->name, $msg) for $self->connected_ircs;
167              
168 0         0 my ($w, $t);
169             my $shutdown = sub {
170 0     0   0 undef $w;
171 0         0 undef $t;
172 0         0 $self->shutdown;
173 0 0       0 $cb->() if $cb;
174 0         0 };
175              
176 0 0   0   0 $w = AE::idle sub {$shutdown->() unless $self->connected_ircs};
  0         0  
177 0         0 $t = AE::timer 3, 0, $shutdown;
178             }
179              
180             sub shutdown {
181 0     0 0 0 my $self = shift;
182              
183 0         0 $self->_ircs([]);
184 0         0 $_->close for @{$self->streams};
  0         0  
185 0         0 $self->streams([]);
186             }
187              
188             sub tab_order {
189 0     0 0 0 my ($self, $window_ids) = @_;
190 0         0 my $order = [];
191 0         0 for my $count (0 .. scalar @$window_ids - 1) {
192 0 0       0 if (my $window = $self->get_window($window_ids->[$count])) {
193 0 0       0 next unless $window->is_channel;
194 0         0 push @$order, $window->id;
195             }
196             }
197 0         0 $self->config->order($order);
198 0         0 $self->config->write;
199             }
200              
201             sub window_nicks {
202 0     0 0 0 my ($self, $window) = @_;
203 0 0       0 return () if $window->type eq "info";
204              
205 0         0 my $irc = $self->get_irc($window->network);
206 0 0 0     0 if ($irc and $irc->is_connected) {
207 0 0       0 if ($window->type eq "channel") {
208 0         0 return $irc->channel_nicks($window->title);
209             }
210             else {
211 0         0 return ($irc->nick, $window->title);
212             }
213             }
214             }
215              
216             sub connect_actions {
217 0     0 0 0 my $self = shift;
218 0         0 map {
219 0         0 $_->join_action,
220             $_->nicks_action($self->window_nicks($_))
221             } $self->windows;
222             }
223              
224             sub find_window {
225 4     4 0 20 my ($self, $title, $irc) = @_;
226 4 50       21 return $self->info_window if $title eq "info";
227 4         29 my $id = $self->_build_window_id($title, $irc->name);
228 4 100       53 if (my $window = $self->get_window($id)) {
229 3         18 return $window;
230             }
231             }
232              
233             sub alert {
234 0     0 0 0 my ($self, $message) = @_;
235 0 0       0 return unless $message;
236 0         0 $self->broadcast({
237             type => "action",
238             event => "alert",
239             body => $message,
240             });
241             }
242              
243             sub create_window {
244 2     2 0 16 my ($self, $title, $irc) = @_;
245 2         34 my $id = $self->_build_window_id($title, $irc->name);
246             my $window = Alice::Window->new(
247             title => $title,
248             type => $irc->is_channel($title) ? "channel" : "privmsg",
249             network => $irc->name,
250             id => $id,
251             buffer => $self->_build_window_buffer($id),
252 0     0   0 render => sub { $self->render(@_) },
253 2 50       54 );
254 2 50       26 if ($window->is_channel) {
255 0         0 my $config = $self->config->servers->{$window->network};
256 0         0 $config->{channels} = [uniq $title, @{$config->{channels}}];
  0         0  
257 0         0 $self->config->write;
258             }
259 2         24 $self->add_window($window);
260 2         7 return $window;
261             }
262              
263             sub _build_window_buffer {
264 3     3   101 my ($self, $id) = @_;
265 3         126 Alice::MessageBuffer->new(
266             id => $id,
267             store => $self->message_store,
268             );
269             }
270              
271             sub _build_window_id {
272 9     9   1029 my ($self, $title, $network) = @_;
273 9         358 md5_hex(encode_utf8(lc $self->user."-$title-$network"));
274             }
275              
276             sub find_or_create_window {
277 2     2 0 6 my ($self, $title, $irc) = @_;
278 2 50       7 return $self->info_window if $title eq "info";
279              
280 2 100       8 if (my $window = $self->find_window($title, $irc)) {
281 1         5 return $window;
282             }
283              
284 1         8 $self->create_window($title, $irc);
285             }
286              
287             sub sorted_windows {
288 1     1 0 3 my $self = shift;
289              
290 0         0 my %o = map {
291 1         19 $self->config->order->[$_] => sprintf "%02d", $_ + 2
292 1         4 } (0 .. @{$self->config->order} - 1);
293              
294 1         27 $o{$self->info_window->id} = "01";
295 1         9 my $prefix = scalar @{$self->config->order} + 1;
  1         6  
296              
297 2         10 map {$_->[1]}
  1         6  
298 2   66     33 sort {$a->[0] cmp $b->[0]}
299 1         9 map {[($o{$_->id} || $o{$_->title} || $prefix.$_->sort_name), $_]}
300             $self->windows;
301             }
302              
303             sub close_window {
304 1     1 0 8 my ($self, $window) = @_;
305              
306 1         14 AE::log debug => "sending a request to close a tab: " . $window->title;
307 1         112 $self->broadcast($window->close_action);
308              
309 1 50       7 if ($window->is_channel) {
310 0         0 my $irc = $self->get_irc($window->network);
311 0         0 my $config = $self->config->servers->{$window->network};
312 0         0 $config->{channels} = [grep {$_ ne $window->title} @{$config->{channels}}];
  0         0  
  0         0  
313 0         0 $self->config->write;
314             }
315              
316 1 50       25 $self->remove_window($window->id) if $window->type ne "info";
317             }
318              
319             sub add_irc_server {
320 1     1 0 15 my ($self, $name, $config) = @_;
321 1         10 $self->config->servers->{$name} = $config;
322 1         14 my $irc = Alice::IRC->new(name => $name);
323 1         7 $self->add_irc($irc);
324 1 50       7 $self->connect_irc($name) if $config->{autoconnect};
325             }
326              
327             sub reload_config {
328 0     0 0 0 my ($self, $new_config) = @_;
329              
330 0   0     0 my %prev = map {$_ => $self->config->servers->{$_}{ircname} || ""}
  0         0  
331 0         0 keys %{ $self->config->servers };
332              
333 0 0       0 if ($new_config) {
334 0         0 $self->config->merge($new_config);
335 0         0 $self->config->write;
336             }
337            
338 0         0 for my $network (keys %{$self->config->servers}) {
  0         0  
339 0         0 my $config = $self->config->servers->{$network};
340 0 0       0 if (!$self->has_irc($network)) {
341 0         0 $self->add_irc_server($network, $config);
342             }
343             else {
344 0         0 my $irc = $self->get_irc($network);
345 0   0     0 $config->{ircname} ||= "";
346 0 0       0 if ($config->{ircname} ne $prev{$network}) {
347 0         0 $irc->update_realname($config->{ircname});
348             }
349             }
350             }
351 0         0 for my $irc ($self->ircs) {
352 0         0 my $name = $irc->name;
353 0 0       0 unless (exists $self->config->servers->{$name}) {
354 0         0 $self->send_info("config", "removing $name server");
355 0 0       0 if ($irc->is_disconnected) {
356 0 0       0 $self->cancel_reconnect($name) if $irc->reconnect_timer;
357 0         0 $irc->cl(undef);
358 0         0 $self->remove_irc($name);
359             }
360             else {
361 0         0 $irc->removed(1);
362 0         0 $self->disconnect_irc($name);
363             }
364             }
365             }
366             }
367              
368             sub announce {
369 0     0 0 0 my ($self, $window, $body) = @_;
370 0         0 $self->broadcast({
371             type => "action",
372             event => "announce",
373             body => $body
374             });
375             }
376              
377             sub send_message {
378 0     0 0 0 my ($self, $window, $nick, $body) = @_;
379              
380 0         0 my $irc = $self->get_irc($window->network);
381 0   0     0 my @messages = $window->format_message($nick, $body,
382             monospaced => $self->is_monospace_nick($nick),
383             self => $irc->nick eq $nick,
384             avatar => $irc->nick_avatar($nick) || "",
385             highlight => $self->is_highlight($irc->nick, $body),
386             );
387              
388 0 0       0 if ($messages[0]->{highlight}) {
389 0         0 push @messages, $self->info_window->format_message(
390             $nick, $body, self => 1, source => $nick);
391             }
392              
393 0         0 $self->broadcast(@messages);
394             }
395              
396             sub send_info {
397 1     1 0 5 my ($self, $network, $body, %options) = @_;
398 1 50       4 return unless $body;
399 1         13 my $message = $self->info_window->format_message($network, $body, %options);
400 1         23 $self->broadcast($message);
401             }
402              
403             sub broadcast {
404 2     2 0 8 my ($self, @messages) = @_;
405 2 50 33     16 return if $self->no_streams or !@messages;
406 0         0 for my $stream (@{$self->streams}) {
  0         0  
407 0         0 $stream->send(\@messages);
408             }
409             }
410              
411             sub ping {
412 0     0 0 0 my $self = shift;
413 0 0       0 return if $self->no_streams;
414 0         0 $_->ping for grep {$_->is_xhr} @{$self->streams};
  0         0  
  0         0  
415             }
416              
417             sub update_window {
418 0     0 0 0 my ($self, $stream, $window, $max, $min, $limit, $total, $cb) = @_;
419              
420 0         0 my $step = 20;
421 0 0       0 if ($limit - $total < 20) {
422 0         0 $step = $limit - $total;
423             }
424              
425             $window->buffer->messages($max, $min, $step, sub {
426 0     0   0 my $msgs = shift;
427              
428 0         0 $stream->send([{
429             window => $window->serialized,
430             type => "chunk",
431             range => (@$msgs ? [$msgs->[0]{msgid}, $msgs->[-1]{msgid}] : []),
432 0 0       0 html => join "", map {$_->{html}} @$msgs,
433             }]);
434              
435 0         0 $total += $step;
436              
437 0 0 0     0 if (@$msgs == $step and $total < $limit) {
438 0         0 $max = $msgs->[0]->{msgid} - 1;
439 0         0 $self->update_window($stream, $window, $max, $min, $limit, $total, $cb);
440             }
441             else {
442 0 0       0 $cb->() if $cb;
443 0         0 return;
444             }
445 0         0 });
446             }
447              
448             sub handle_message {
449 0     0 0 0 my ($self, $message) = @_;
450              
451 0 0       0 if (my $window = $self->get_window($message->{source})) {
452 0     0   0 my $stream = first {$_->id eq $message->{stream}} @{$self->streams};
  0         0  
  0         0  
453 0 0       0 return unless $stream;
454              
455 0 0       0 $message->{msg} = html_to_irc($message->{msg}) if $message->{html};
456              
457 0         0 for my $line (split /\n/, $message->{msg}) {
458 0 0       0 next unless $line;
459              
460 0         0 my $input = Alice::Request->new(
461             window => $window,
462             stream => $stream,
463             line => $line,
464             );
465              
466 0         0 $self->irc_command($input);
467             }
468             }
469             }
470              
471             sub purge_disconnects {
472 0     0 0 0 my ($self) = @_;
473 0         0 AE::log debug => "removing broken streams";
474 0         0 $self->streams([grep {!$_->closed} @{$self->streams}]);
  0         0  
  0         0  
475             }
476              
477             sub render {
478 1     1 0 9 my ($self, $template, @data) = @_;
479 1         15 $self->template->render_file("$template.html", $self, @data)->as_string;
480             }
481              
482             sub is_highlight {
483 0     0 0 0 my ($self, $own_nick, $body) = @_;
484 0         0 $body = filter_colors $body;
485 0     0   0 any {$body =~ /(?:\W|^)\Q$_\E(?:\W|$)/i }
486 0         0 (@{$self->config->highlights}, $own_nick);
  0         0  
487             }
488              
489             sub is_monospace_nick {
490 0     0 0 0 my ($self, $nick) = @_;
491 0     0   0 any {$_ eq $nick} @{$self->config->monospace_nicks};
  0         0  
  0         0  
492             }
493              
494             sub is_ignore {
495 2     2 0 4 my $self = shift;
496 2         12 return $self->config->is_ignore(@_);
497             }
498              
499             sub add_ignore {
500 1     1 0 3 my $self = shift;
501 1         35 return $self->config->add_ignore(@_);
502             }
503              
504             sub remove_ignore {
505 1     1 0 2 my $self = shift;
506 1         17 return $self->config->remove_ignore(@_);
507             }
508              
509             sub ignores {
510 0     0 0   my $self = shift;
511 0           return $self->config->ignores(@_);
512             }
513              
514             sub static_url {
515 0     0 0   my ($self, $file) = @_;
516 0           return $self->config->static_prefix . $file;
517             }
518              
519             sub auth_enabled {
520 0     0 0   my $self = shift;
521              
522             # cache it
523 0 0         if (!defined $self->{_auth_enabled}) {
524 0   0       $self->{_auth_enabled} = ($self->config->auth
525             and ref $self->config->auth eq 'HASH'
526             and $self->config->auth->{user}
527             and $self->config->auth->{pass});
528             }
529              
530 0           return $self->{_auth_enabled};
531             }
532              
533             sub authenticate {
534 0     0 0   my ($self, $user, $pass) = @_;
535 0   0       $user ||= "";
536 0   0       $pass ||= "";
537 0 0         if ($self->auth_enabled) {
538 0   0       return ($self->config->auth->{user} eq $user
539             and $self->config->auth->{pass} eq $pass);
540             }
541 0           return 1;
542             }
543              
544             sub set_away {
545 0     0 0   my ($self, $message) = @_;
546 0 0         my @args = (defined $message ? (AWAY => $message) : "AWAY");
547 0           $_->send_srv(@args) for $self->connected_ircs;
548             }
549              
550             sub tabsets {
551 0     0 0   my $self = shift;
552 0           map {
553 0           Alice::Tabset->new(
554             name => $_,
555             windows => $self->config->tabsets->{$_},
556             );
557 0           } sort keys %{$self->config->tabsets};
558             }
559              
560             __PACKAGE__->meta->make_immutable;
561             1;