File Coverage

blib/lib/App/Alice.pm
Criterion Covered Total %
statement 142 245 57.9
branch 16 62 25.8
condition 5 32 15.6
subroutine 46 64 71.8
pod 2 45 4.4
total 211 448 47.1


line stmt bran cond sub pod time code
1             package App::Alice;
2              
3 4     4   81098 use Text::MicroTemplate::File;
  4         30487  
  4         182  
4 4     4   2571 use App::Alice::Window;
  4         14  
  4         158  
5 4     4   2491 use App::Alice::InfoWindow;
  4         11  
  4         110  
6 4     4   2312 use App::Alice::HTTPD;
  4         15  
  4         141  
7 4     4   2809 use App::Alice::IRC;
  4         19  
  4         151  
8 4     4   2626 use App::Alice::Signal;
  4         11  
  4         108  
9 4     4   2222 use App::Alice::Config;
  4         17  
  4         780  
10 4     4   3875 use App::Alice::Logger;
  4         14  
  4         178  
11 4     4   2682 use App::Alice::History;
  4         18  
  4         144  
12 4     4   47 use Any::Moose;
  4         11  
  4         36  
13 4     4   6032 use File::Copy;
  4         10431  
  4         304  
14 4     4   26 use Digest::MD5 qw/md5_hex/;
  4         7  
  4         177  
15 4     4   23 use List::Util qw/first/;
  4         7  
  4         258  
16 4     4   23 use Encode;
  4         9  
  4         16500  
17              
18             our $VERSION = '0.19';
19              
20             has condvar => (
21             is => 'rw',
22             isa => 'AnyEvent::CondVar'
23             );
24              
25             has config => (
26             is => 'ro',
27             isa => 'App::Alice::Config',
28             );
29              
30             has msgid => (
31             is => 'rw',
32             isa => 'Int',
33             default => 1,
34             );
35              
36 18     18 0 508 sub next_msgid {$_[0]->msgid($_[0]->msgid + 1)}
37              
38             has _ircs => (
39             is => 'rw',
40             isa => 'ArrayRef',
41             default => sub {[]},
42             );
43              
44 4     4 0 8 sub ircs {@{$_[0]->_ircs}}
  4         38  
45 3     3 0 17 sub add_irc {push @{$_[0]->_ircs}, $_[1]}
  3         22  
46 1     1 0 8 sub has_irc {$_[0]->get_irc($_[1])}
47 3     3 0 19 sub get_irc {first {$_->alias eq $_[1]} $_[0]->ircs}
  3     3   21  
48 0     0 0 0 sub remove_irc {$_[0]->_ircs([ grep { $_->alias ne $_[1] } $_[0]->ircs])}
  0         0  
49 0     0 0 0 sub irc_aliases {map {$_->alias} $_[0]->ircs}
  0         0  
50 0     0 0 0 sub connected_ircs {grep {$_->is_connected} $_[0]->ircs}
  0         0  
51              
52             has standalone => (
53             is => 'ro',
54             isa => 'Bool',
55             default => 1,
56             );
57              
58             has httpd => (
59             is => 'rw',
60             isa => 'App::Alice::HTTPD',
61             lazy => 1,
62             default => sub {
63             App::Alice::HTTPD->new(app => shift);
64             },
65             );
66              
67             has commands => (
68             is => 'ro',
69             isa => 'App::Alice::Commands',
70             lazy => 1,
71             default => sub {
72             App::Alice::Commands->new(app => shift);
73             }
74             );
75              
76             has notifier => (
77             is => 'ro',
78             lazy => 1,
79             default => sub {
80             my $self = shift;
81             my $notifier;
82             eval {
83             if ($^O eq 'darwin') {
84             # 5.10 doesn't seem to put Extras in @INC
85             # need this for Foundation.pm
86             if ($] >= 5.01 and -e "/System/Library/Perl/Extras/5.10.0") {
87             require lib;
88             lib->import("/System/Library/Perl/Extras/5.10.0");
89             }
90             require App::Alice::Notifier::Growl;
91             $notifier = App::Alice::Notifier::Growl->new;
92             }
93             elsif ($^O eq 'linux') {
94             require App::Alice::Notifier::LibNotify;
95             $notifier = App::Alice::Notifier::LibNotify->new;
96             }
97             };
98             $self->log(info => "Notifications disabled") unless $notifier;
99             return $notifier;
100             }
101             );
102              
103             has history => (
104             is => 'rw',
105             lazy => 1,
106             default => sub {
107             my $self = shift;
108             my $config = $self->config->path."/log.db";
109             if (-e $config) {
110             if ((stat($config))[9] < 1272757679) {
111             print STDERR "Log schema is out of date, updating\n";
112             copy($self->config->assetdir."/log.db", $config);
113             }
114             }
115             else {
116             copy($self->config->assetdir."/log.db", $config);
117             }
118             App::Alice::History->new(
119             dbfile => $self->config->path ."/log.db"
120             );
121             },
122             );
123              
124             sub store {
125 2     2 0 5 my $self = shift;
126 2         11 my %fields = @_;
127 2         8 $fields{user} = $self->user;
128 2         6 $fields{time} = time;
129 2         21 $self->history->store(%fields);
130             }
131              
132             has logger => (
133             is => 'ro',
134             default => sub {App::Alice::Logger->new},
135             );
136              
137 11 50   11 0 75 sub log {$_[0]->logger->log($_[1] => $_[2]) if $_[0]->config->show_debug}
138              
139             has _windows => (
140             is => 'rw',
141             isa => 'ArrayRef',
142             default => sub {[]},
143             );
144              
145 37     37 0 61 sub windows {@{$_[0]->_windows}}
  37         201  
146 9     9 0 14 sub add_window {push @{$_[0]->_windows}, $_[1]}
  9         38  
147 2     2 0 26 sub has_window {$_[0]->get_window($_[1])}
148 65     65 0 307 sub get_window {first {$_->id eq $_[1]} $_[0]->windows}
  27     27   288  
149 4     4 0 40 sub remove_window {$_[0]->_windows([grep {$_->id ne $_[1]} $_[0]->windows])}
  9         44  
150 0     0 0 0 sub window_ids {map {$_->id} $_[0]->windows}
  0         0  
151              
152             has 'template' => (
153             is => 'ro',
154             isa => 'Text::MicroTemplate::File',
155             lazy => 1,
156             default => sub {
157             my $self = shift;
158             Text::MicroTemplate::File->new(
159             include_path => $self->config->assetdir . '/templates',
160             cache => 1,
161             );
162             },
163             );
164              
165             has 'info_window' => (
166             is => 'ro',
167             isa => 'App::Alice::InfoWindow',
168             lazy => 1,
169             default => sub {
170             my $self = shift;
171             my $info = App::Alice::InfoWindow->new(
172             assetdir => $self->config->assetdir,
173             app => $self,
174             );
175             $self->add_window($info);
176             return $info;
177             }
178             );
179              
180             has 'shutting_down' => (
181             is => 'rw',
182             default => 0,
183             isa => 'Bool',
184             );
185              
186             has 'user' => (
187             is => 'ro',
188             default => $ENV{USER}
189             );
190              
191             sub BUILDARGS {
192 3     3 1 3373 my ($class, %options) = @_;
193 3         10 my $self = {standalone => 1};
194 3         9 for (qw/standalone history notifier template user/) {
195 15 100       41 if (exists $options{$_}) {
196 6         11 $self->{$_} = $options{$_};
197 6         12 delete $options{$_};
198             }
199             }
200 3         40 $self->{config} = App::Alice::Config->new(%options);
201             # some options get overwritten by the config
202             # so merge options again
203 3         20 $self->{config}->merge(\%options);
204 3         85 return $self;
205             }
206              
207             sub run {
208 0     0 0 0 my $self = shift;
209             # initialize lazy stuff
210 0         0 $self->commands;
211 0         0 $self->history;
212 0         0 $self->info_window;
213 0         0 $self->template;
214 0         0 $self->httpd;
215 0         0 $self->notifier;
216              
217 0 0       0 print STDERR "Location: http://".$self->config->http_address.":".$self->config->http_port."/\n"
218             if $self->standalone;
219              
220 0         0 $self->add_irc_server($_, $self->config->servers->{$_})
221 0         0 for keys %{$self->config->servers};
222            
223 0 0       0 if ($self->standalone) {
224 0         0 $self->condvar(AnyEvent->condvar);
225 0         0 my @sigs;
226 0         0 for my $sig (qw/INT QUIT/) {
227 0     0   0 my $w = AnyEvent->signal(
228             signal => $sig,
229             cb => sub {App::Alice::Signal->new(app => $self, type => $sig)}
230 0         0 );
231 0         0 push @sigs, $w;
232             }
233              
234 0         0 $self->condvar->recv;
235             }
236             }
237              
238             sub init_shutdown {
239 0     0 0 0 my ($self, $cb, $msg) = @_;
240 0         0 $self->{on_shutdown} = $cb;
241 0         0 $self->shutting_down(1);
242 0         0 $self->history(undef);
243 0         0 $self->alert("Alice server is shutting down");
244 0 0       0 if ($self->connected_ircs) {
245 0 0       0 print STDERR "\nDisconnecting, please wait\n" if $self->standalone;
246 0         0 $_->init_shutdown($msg) for $self->connected_ircs;
247             }
248             else {
249 0         0 print "\n";
250 0         0 $self->shutdown;
251 0         0 return;
252             }
253 0     0   0 $self->{shutdown_timer} = AnyEvent->timer(
254             after => 3,
255             cb => sub{$self->shutdown}
256 0         0 );
257             }
258              
259             sub shutdown {
260 0     0 0 0 my $self = shift;
261 0         0 $self->_ircs([]);
262 0         0 $self->httpd->shutdown;
263 0         0 $_->buffer->clear for $self->windows;
264 0 0       0 delete $self->{shutdown_timer} if $self->{shutdown_timer};
265 0 0       0 $self->{on_shutdown}->() if $self->{on_shutdown};
266 0 0       0 $self->condvar->send if $self->condvar;
267             }
268              
269             sub handle_command {
270 0     0 0 0 my ($self, $command, $window) = @_;
271 0         0 $self->commands->handle($command, $window);
272             }
273              
274             sub reload_commands {
275 0     0 0 0 my $self = shift;
276 0         0 $self->commands->reload_handlers;
277             }
278              
279             sub merge_config {
280 0     0 0 0 my ($self, $new_config) = @_;
281 0         0 for my $newserver (values %$new_config) {
282 0 0       0 if (! exists $self->config->servers->{$newserver->{name}}) {
283 0         0 $self->add_irc_server($newserver->{name}, $newserver);
284             }
285 0         0 for my $key (keys %$newserver) {
286 0         0 $self->config->servers->{$newserver->{name}}{$key} = $newserver->{$key};
287             }
288             }
289             }
290              
291             sub tab_order {
292 0     0 0 0 my ($self, $window_ids) = @_;
293 0         0 my $order = [];
294 0         0 for my $count (0 .. scalar @$window_ids - 1) {
295 0 0       0 if (my $window = $self->get_window($window_ids->[$count])) {
296 0 0 0     0 next unless $window->is_channel
297             and $self->config->servers->{$window->irc->alias};
298 0         0 push @$order, $window->title;
299             }
300             }
301 0         0 $self->config->order($order);
302 0         0 $self->config->write;
303             }
304              
305             sub with_messages {
306 0     0 0 0 my ($self, $cb) = @_;
307 0         0 $_->buffer->with_messages($cb) for $self->windows;
308             }
309              
310             sub find_window {
311 24     24 0 3310 my ($self, $title, $connection) = @_;
312 24 50       68 return $self->info_window if $title eq "info";
313 24         101 my $id = $self->_build_window_id($title, $connection->alias);
314 24 100       319 if (my $window = $self->get_window($id)) {
315 18         111 return $window;
316             }
317             }
318              
319             sub alert {
320 0     0 0 0 my ($self, $message) = @_;
321 0 0       0 return unless $message;
322 0         0 $self->broadcast({
323             type => "action",
324             event => "alert",
325             body => $message,
326             });
327             }
328              
329             sub create_window {
330 7     7 0 1006 my ($self, $title, $connection) = @_;
331 7         37 my $id = $self->_build_window_id($title, $connection->alias);
332 7         175 my $window = App::Alice::Window->new(
333             title => $title,
334             irc => $connection,
335             assetdir => $self->config->assetdir,
336             app => $self,
337             );
338 7         33 $self->add_window($window);
339 7         25 return $window;
340             }
341              
342             sub _build_window_id {
343 41     41   1167 my ($self, $title, $session) = @_;
344 41         3503 md5_hex(encode_utf8(lc $self->user."-$title-$session"));
345             }
346              
347             sub find_or_create_window {
348 7     7 0 12 my ($self, $title, $connection) = @_;
349 7 50       25 return $self->info_window if $title eq "info";
350 7 100       22 if (my $window = $self->find_window($title, $connection)) {
351 3         13 return $window;
352             }
353             else {
354 4         87 $self->create_window($title, $connection);
355             }
356             }
357              
358             sub sorted_windows {
359 1     1 0 3 my $self = shift;
360 1         3 my %o;
361 1 50       8 if ($self->config->order) {
362 0         0 %o = map {$self->config->order->[$_] => $_ + 2}
  1         6  
363 1         4 0 .. @{$self->config->order} - 1;
364             }
365 1         2 $o{info} = 1;
366 1   33     3 sort { ($o{$a->title} || $a->sort_name) cmp ($o{$b->title} || $b->sort_name) }
  1   33     19  
367             $self->windows;
368             }
369              
370             sub close_window {
371 2     2 0 10 my ($self, $window) = @_;
372 2         13 $self->broadcast($window->close_action);
373 2 50       21 $self->log(debug => "sending a request to close a tab: " . $window->title)
374             if $self->httpd->stream_count;
375 2 50       23 $self->remove_window($window->id) if $window->type ne "info";
376             }
377              
378             sub add_irc_server {
379 2     2 0 23 my ($self, $name, $config) = @_;
380 2         49 $self->config->servers->{$name} = $config;
381 2         26 my $irc = App::Alice::IRC->new(
382             app => $self,
383             alias => $name
384             );
385 2         12 $self->add_irc($irc);
386             }
387              
388             sub reload_config {
389 0     0 0 0 my ($self, $new_config) = @_;
390              
391 0   0     0 my %prev = map {$_ => $self->config->servers->{$_}{ircname} || ""}
  0         0  
392 0         0 keys %{ $self->config->servers };
393              
394 0 0       0 if ($new_config) {
395 0         0 $self->config->merge($new_config);
396 0         0 $self->config->write;
397             }
398            
399 0         0 for my $network (keys %{$self->config->servers}) {
  0         0  
400 0         0 my $config = $self->config->servers->{$network};
401 0 0       0 if (!$self->has_irc($network)) {
402 0         0 $self->add_irc_server($network, $config);
403             }
404             else {
405 0         0 my $irc = $self->get_irc($network);
406 0   0     0 $config->{ircname} ||= "";
407 0 0       0 if ($config->{ircname} ne $prev{$network}) {
408 0         0 $irc->update_realname($config->{ircname});
409             }
410 0         0 $irc->config($config);
411             }
412             }
413 0         0 for my $irc ($self->ircs) {
414 0 0       0 if (!$self->config->servers->{$irc->alias}) {
415 0         0 $self->remove_window($_->id) for $irc->windows;
416 0         0 $irc->remove;
417             }
418             }
419             }
420              
421             sub format_info {
422 9     9 0 24 my ($self, $session, $body, %options) = @_;
423 9         65 $self->info_window->format_message($session, $body, %options);
424             }
425              
426             sub broadcast {
427 22     22 0 54 my ($self, @messages) = @_;
428            
429             # add any highlighted messages to the log window
430 0         0 push @messages, map {$self->info_window->copy_message($_)}
  25         66  
431 22         47 grep {$_->{highlight}} @messages;
432            
433 22         175 $self->httpd->broadcast(@messages);
434            
435 22 50 33     335 if ($self->config->alerts and $self->notifier and ! $self->httpd->stream_count) {
      33        
436 0         0 for my $message (@messages) {
437 0 0 0     0 next if !$message->{window} or $message->{window}{type} eq "info";
438 0 0       0 $self->notifier->display($message) if $message->{highlight};
439             }
440             }
441             }
442              
443             sub render {
444 27     27 0 67 my ($self, $template, @data) = @_;
445 27         259 return $self->template->render_file("$template.html", $self, @data)->as_string;
446             }
447              
448             sub is_highlight {
449 2     2 0 5 my ($self, $own_nick, $body) = @_;
450 2         4 for ((@{$self->config->highlights}, $own_nick)) {
  2         14  
451 2         5 my $highlight = quotemeta($_);
452 2 50       84 return 1 if $body =~ /\b$highlight\b/i;
453             }
454 2         7 return 0;
455             }
456              
457             sub is_monospace_nick {
458 2     2 0 4 my ($self, $nick) = @_;
459 2         5 for (@{$self->config->monospace_nicks}) {
  2         17  
460 2 50       9 return 1 if $_ eq $nick;
461             }
462 2         7 return 0;
463             }
464              
465             sub is_ignore {
466 4     4 0 10 my ($self, $nick) = @_;
467 4         32 for ($self->config->ignores) {
468 1 50       8 return 1 if $nick eq $_;
469             }
470 3         17 return 0;
471             }
472              
473             sub add_ignore {
474 1     1 0 3 my ($self, $nick) = @_;
475 1         9 $self->config->add_ignore($nick);
476 1         7 $self->config->write;
477             }
478              
479             sub remove_ignore {
480 1     1 0 3 my ($self, $nick) = @_;
481 1         9 $self->config->ignore([ grep {$nick ne $_} $self->config->ignores ]);
  1         6  
482 1         6 $self->config->write;
483             }
484              
485             sub ignores {
486 2     2 1 5 my $self = shift;
487 2         16 return $self->config->ignores;
488             }
489              
490             sub auth_enabled {
491 2     2 0 4 my $self = shift;
492 2   33     48 return ($self->config->auth
493             and ref $self->config->auth eq 'HASH'
494             and $self->config->auth->{user}
495             and $self->config->auth->{pass});
496             }
497              
498             sub authenticate {
499 0     0 0   my ($self, $user, $pass) = @_;
500 0   0       $user ||= "";
501 0   0       $pass ||= "";
502 0 0         if ($self->auth_enabled) {
503 0   0       return ($self->config->auth->{user} eq $user
504             and $self->config->auth->{pass} eq $pass);
505             }
506 0           return 1;
507             }
508              
509             sub static_url {
510 0     0 0   my ($self, $file) = @_;
511 0           return $self->config->static_prefix . $file;
512             }
513              
514             __PACKAGE__->meta->make_immutable;
515             1;