File Coverage

blib/lib/Convos.pm
Criterion Covered Total %
statement 136 161 84.4
branch 33 52 63.4
condition 14 33 42.4
subroutine 20 24 83.3
pod 1 1 100.0
total 204 271 75.2


line stmt bran cond sub pod time code
1             package Convos;
2              
3             =head1 NAME
4              
5             Convos - Multiuser IRC proxy with web interface
6              
7             =head1 VERSION
8              
9             0.8604
10              
11             =head1 DESCRIPTION
12              
13             Convos is to a multi-user IRC Proxy, that also provides a easy to use Web
14             interface. Feature list:
15              
16             =over 4
17              
18             =item * Always online
19              
20             The backend server will keep you logged in and logs all the activity
21             in your archive.
22              
23             =item * Archive
24              
25             All chats will be logged and indexed, which allow you to search in
26             earlier conversations.
27              
28             =item * Avatars
29              
30             The chat contains profile pictures which can be retrieved from Facebook
31             or from gravatar.com.
32              
33             =item * Include external resources
34              
35             Links to images and video will be displayed inline. No need to click on
36             the link to view the data.
37              
38             =back
39              
40             =head2 Architecture principles
41              
42             =over 4
43              
44             =item * Keep the JS simple and manageable
45              
46             =item * Use Redis to manage state / publish subscribe
47              
48             =item * Bootstrap-based user interface
49              
50             =back
51              
52             =head1 RUNNING CONVOS
53              
54             Convos has sane defaults so after installing L you should be
55             able to just run it:
56              
57             # Install
58             $ cpanm Convos
59             # Run it
60             $ convos daemon --listen http://*:8080
61              
62             The steps above will install and run Convos in a single process. This is a
63             very quick way to get started, but we incourage to run Convos as one backend
64             and one frontend:
65              
66             # Start the backend first
67             $ convos backend start
68              
69             # Then start the frontend
70             $ convos daemon --listen http://*:8080
71              
72             This allow you to upgrade and restart the frontend, without having to
73             reconnect to the IRC servers.
74              
75             See L for more details.
76              
77             =head1 CUSTOM TEMPLATES
78              
79             Some parts of the Convos templates can include custom content. Example:
80              
81             # Create a directory where you can store the templates
82             $ mkdir -p custom-convos/vendor
83              
84             # Edit the template you want to customize
85             $ $EDITOR custom-convos/vendor/login_footer.html.ep
86              
87             # Start convos with CONVOS_TEMPLATES set. Without /vendor at the end
88             $ CONVOS_TEMPLATES=$PWD/custom-convos convos daemon --listen http://*:5000
89              
90             Any changes to the templates require the server to restart.
91              
92             The templates that can be customized are:
93              
94             =over 4
95              
96             =item * vendor/login_footer.html.ep
97              
98             This template will be included below the form on the C page.
99              
100             =item * vendor/register_footer.html.ep
101              
102             This template will be included below the form on the C page.
103              
104             =item * vendor/wizard.html.ep
105              
106             This template will be included below the form on the C page that a
107             new visitor sees after registering.
108              
109             =back
110              
111             =head1 RESOURCES
112              
113             =over 4
114              
115             =item * Homepage: L
116              
117             =item * Project page: L
118              
119             =item * Icon: L
120              
121             =item * Logo: L
122              
123             =back
124              
125             =head1 SEE ALSO
126              
127             =over 4
128              
129             =item * L
130              
131             Mojolicious controller for IRC chat.
132              
133             =item * L
134              
135             Mojolicious controller for user data.
136              
137             =item * L
138              
139             Backend functionality.
140              
141             =back
142              
143             =cut
144              
145 36     36   8232421 use Mojo::Base 'Mojolicious';
  36         75  
  36         200  
146 36     36   3556284 use Mojo::Redis;
  36         84356  
  36         266  
147 36     36   910 use Mojo::Util qw( md5_sum );
  36         53  
  36         1964  
148 36     36   165 use File::Spec::Functions qw( catdir catfile tmpdir );
  36         48  
  36         1819  
149 36     36   167 use File::Basename qw( dirname );
  36         53  
  36         1394  
150 36     36   16391 use Convos::Core;
  36         90  
  36         358  
151 36     36   1006 use Convos::Core::Util ();
  36         54  
  36         1673  
152              
153             our $VERSION = '0.8604';
154              
155             $ENV{CONVOS_DEFAULT_CONNECTION} //= 'chat.freenode.net:6697';
156              
157             { # required before Mojo::Redis 1.01
158 36     36   162 no warnings 'redefine';
  36         52  
  36         63581  
159 0     0   0 *Mojo::Redis::emit_safe = sub { shift->emit(@_) };
160             }
161              
162             =head1 ATTRIBUTES
163              
164             =head2 core
165              
166             Holds a L object .
167              
168             =head2 upgrader
169              
170             DEPRECATED.
171              
172             =cut
173              
174             has core => sub {
175             my $self = shift;
176             my $core = Convos::Core->new(redis => $self->redis);
177              
178             $core->log($self->log);
179             $core->archive->log_dir($ENV{CONVOS_ARCHIVE_DIR} || $self->home->rel_dir('irc_logs'));
180             $core;
181             };
182              
183             has upgrader => sub { die "upgrader() is deprecated" };
184              
185             =head1 METHODS
186              
187             =head2 startup
188              
189             This method will run once at server start
190              
191             =cut
192              
193             sub startup {
194 45     45 1 589421 my $self = shift;
195 45         102 my $config;
196              
197 45         309 $self->{convos_executable_path} = $0; # required to work from within toadfarm
198 45         181 $self->_from_cpan;
199 45         2744 $config = $self->_config;
200              
201 45 50       160 if (my $log = $config->{log}) {
202 0 0       0 $self->log->level($log->{level}) if $log->{level};
203 0 0 0     0 $self->log->path($log->{file}) if $log->{file} ||= $log->{path} || $ENV{CONVOS_FRONTEND_LOGFILE};
      0        
204 0         0 delete $self->log->{handle}; # make sure it's fresh to file
205             }
206              
207 45         1050 $self->ua->max_redirects(2); # support getting facebook pictures
208 45         6167 $self->plugin('Convos::Plugin::Helpers');
209 45         3824 $self->secrets([time]); # will be replaced by _set_secrets()
210 45         400 $self->_redis_url;
211              
212 45 100       3223 return if $ENV{CONVOS_BACKEND_ONLY}; # set script/convos when started as backend
213              
214             # frontend code
215 43         1015 $self->sessions->default_expiration(86400 * 30);
216 43 100       2170 $self->sessions->secure(1) if $ENV{CONVOS_SECURE_COOKIES};
217 43         220 $self->_assets;
218 43         559176 $self->_public_routes;
219 43         193 $self->_private_routes;
220              
221 43 50 66     14186 if (!$ENV{CONVOS_INVITE_CODE} and $config->{invite_code}) {
222 0         0 $self->log->warn(
223             "invite_code from config file will be deprecated. Set the CONVOS_INVITE_CODE env variable instead.");
224 0         0 $ENV{CONVOS_INVITE_CODE} = $config->{invite_code};
225             }
226 43 100       218 if ($ENV{CONVOS_TEMPLATES}) {
227              
228             # Using push() since I don't think it's a good idea for allowing the user
229             # to customize every template, at least not when the application is still
230             # unstable.
231 1         18 push @{$self->renderer->paths}, $ENV{CONVOS_TEMPLATES};
  1         40  
232             }
233              
234 43         358 $self->defaults(full_page => 1, organization_name => $self->config('name'));
235 43         1593 $self->hook(before_dispatch => \&_before_dispatch);
236 43 100       1689 $self->_embed_backend if $ENV{CONVOS_BACKEND_EMBEDDED};
237              
238 42         178 Scalar::Util::weaken($self);
239 42     3   603 Mojo::IOLoop->timer(0 => sub { $self->_set_secrets });
  3         24188  
240             }
241              
242             sub _assets {
243 43     43   74 my $self = shift;
244              
245 43         184 $self->plugin('AssetPack');
246 43         682518 $self->plugin('FontAwesome4', css => []);
247 43         66527 $self->asset('c.css' => qw( /scss/font-awesome.scss /sass/convos.scss ));
248 43         201627 $self->asset(
249             'c.js' => qw(
250             https://platform.twitter.com/widgets.js
251             /js/globals.js
252             /js/jquery.js
253             /js/ws-reconnecting.js
254             /js/jquery.hotkeys.js
255             /js/jquery.finger.js
256             /js/jquery.pjax.js
257             /js/jquery.notify.js
258             /js/jquery.disableouterscroll.js
259             /js/convos.events.js
260             /js/convos.sidebar.js
261             /js/convos.socket.js
262             /js/convos.input.js
263             /js/convos.conversations.js
264             /js/convos.nicks.js
265             /js/convos.goto-anything.js
266             /js/convos.chat.js
267             )
268             );
269             }
270              
271             sub _before_dispatch {
272 4     4   180481 my $c = shift;
273              
274 4   33     28 $c->stash(full_page => !($c->req->is_xhr || $c->param('_pjax')));
275              
276 4 50       2658 if (my $base = $c->req->headers->header('X-Request-Base')) {
277 0         0 $c->req->url->base(Mojo::URL->new($base));
278             }
279 4 50       553 if (!$c->app->config->{hostname_is_set}++) {
280 0         0 $c->redis->set('convos:frontend:url' => $c->req->url->base->to_abs->to_string);
281             }
282             }
283              
284             sub _config {
285 45     45   88 my $self = shift;
286 45 50       473 my $config = $ENV{MOJO_CONFIG} ? $self->plugin('Config') : $self->config;
287              
288 45   100     1129 $config->{hypnotoad}{listen} ||= [split /,/, $ENV{MOJO_LISTEN} || 'http://*:8080'];
      50        
289 45 50       171 $config->{hypnotoad}{pid_file} = $ENV{CONVOS_FRONTEND_PID_FILE} if $ENV{CONVOS_FRONTEND_PID_FILE};
290 45 50       151 $config->{hypnotoad}{group} = $ENV{RUN_AS_GROUP} if $ENV{RUN_AS_GROUP};
291 45 50       148 $config->{hypnotoad}{user} = $ENV{RUN_AS_USER} if $ENV{RUN_AS_USER};
292 45 100       145 $config->{name} = $ENV{CONVOS_ORGANIZATION_NAME} if $ENV{CONVOS_ORGANIZATION_NAME};
293 45   100     281 $config->{name} ||= 'Nordaaker';
294 45         85 $config;
295             }
296              
297             sub _embed_backend {
298 3     3   1060 my $self = shift;
299              
300 3 100       29 die "Cannot start embedded backend from hypnotoad" if $SIG{USR2};
301 1         531 require Convos::Control::Backend;
302 1         6 my $backend = Convos::Control::Backend->new;
303              
304 1         4 $backend->read_pid;
305              
306 1 50 33     11 if ($backend->pid and $backend->pid_running) {
307 0         0 $self->app->log->warn('Backend is already running.');
308             }
309             else {
310 1         7 $backend->pid($$);
311 1         7 $backend->write_pid;
312 1         252 $self->{pid_file} = $backend->pid_file;
313 1         22 $self->log->info('Starting convos backend.');
314 1         227 $self->core->start;
315             }
316             }
317              
318             sub _from_cpan {
319 45     45   77 my $self = shift;
320 45         2888 my $home = catdir dirname(__FILE__), 'Convos';
321              
322 45 50       1017 return if -d $self->home->rel_dir('templates');
323 45         3756 $self->home->parse($home);
324 45         2266 $self->static->paths->[0] = $self->home->rel_dir('public');
325 45         3500 $self->renderer->paths->[0] = $self->home->rel_dir('templates');
326             }
327              
328             sub _private_routes {
329 43     43   79 my $self = shift;
330 43         745 my $r = $self->routes->under('/')->to('user#auth', layout => 'view');
331              
332 43         12795 $self->plugin('LinkEmbedder' => {route => $r->get('/oembed')->to(layout => undef)->name('oembed')});
333              
334 43         189214 $r->websocket('/socket')->to('chat#socket')->name('socket');
335 43         22831 $r->get('/chat/command-history')->to('client#command_history');
336 43         24102 $r->get('/chat/notifications')->to('client#notifications', layout => undef)->name('notification.list');
337 43         23361 $r->post('/chat/notifications/clear')->to('client#clear_notifications', layout => undef)->name('notifications.clear');
338 43         24512 $r->any('/connection/add')->to('connection#add_connection')->name('connection.add');
339 43         22228 $r->any('/connection/:name/control')->to('connection#control')->name('connection.control');
340 43         24620 $r->any('/connection/:name/edit')->to('connection#edit_connection')->name('connection.edit');
341 43         23722 $r->get('/connection/:name/delete')->to(template => 'connection/delete_connection', layout => 'tactile');
342 43         23392 $r->post('/connection/:name/delete')->to('connection#delete_connection')->name('connection.delete');
343 43         24243 $r->any('/profile')->to('user#edit')->name('user.edit');
344 43         20791 $r->any('/profile/delete')->to('user#delete')->name('user.delete');
345 43         22797 $r->post('/profile/timezone/offset')->to('user#tz_offset');
346 43         24108 $r->get('/wizard')->to('connection#wizard')->name('wizard');
347              
348 43         20827 my $network_r = $r->any('/:network');
349 43         18313 $network_r->get('/*target' => [target => qr/[\#\&][^\x07\x2C\s]{1,50}/])->to('client#conversation', is_channel => 1)
350             ->name('view');
351 43         21772 $network_r->get('/*target' => [target => qr/[A-Za-z_\-\[\]\\\^\{\}\|\`][A-Za-z0-9_\-\[\]\\\^\{\}\|\`]{1,15}/])
352             ->to('client#conversation', is_channel => 0)->name('view');
353 43         20766 $network_r->get('/')->to('client#conversation')->name('view.network');
354             }
355              
356             sub _public_routes {
357 43     43   154 my $self = shift;
358 43         891 my $r = $self->routes->any->to(layout => 'tactile');
359              
360 43         17370 $r->get('/')->to('client#route')->name('index');
361 43     0   14532 $r->get('/convos')->to(cb => sub { shift->redirect_to('index'); });
  0         0  
362 43         22336 $r->get('/avatar')->to('user#avatar')->name('avatar');
363 43         22330 $r->get('/login')->to('user#login')->name('login');
364 43         21069 $r->post('/login')->to('user#login');
365 43         19796 $r->get('/register/:invite', {invite => ''})->to('user#register')->name('register');
366 43         22622 $r->post('/register/:invite', {invite => ''})->to('user#register');
367 43         22183 $r->get('/logout')->to('user#logout')->name('logout');
368 43         20201 $r;
369             }
370              
371             sub _redis_url {
372 45     45   86 my $self = shift;
373 45         70 my $url;
374              
375 45         115 for my $k (qw( CONVOS_REDIS_URL REDISTOGO_URL REDISCLOUD_URL DOTCLOUD_DATA_REDIS_URL )) {
376 166 100       494 $url = $ENV{$k} or next;
377 7         131 $self->log->debug("Using $k environment variable as Redis connection URL.");
378 7         1120 last;
379             }
380              
381 45 100       162 unless ($url) {
382 38 50       203 if ($self->config('redis')) {
    100          
383 0         0 $self->log->warn("'redis' url from config file will be deprecated. Run 'perldoc Convos' for alternative setup.");
384 0         0 $url = $self->config('redis');
385             }
386             elsif ($self->mode eq 'production') {
387 1         49 $self->log->debug("Using default Redis connection URL redis://127.0.0.1:6379/1");
388 1         59 $url = 'redis://127.0.0.1:6379/1';
389             }
390             else {
391 37         2154 $self->log->debug("Could not find CONVOS_REDIS_URL value.");
392 37         2091 return;
393             }
394             }
395              
396 8         81 $url = Mojo::URL->new($url);
397 8 100 100     2185 $url->path($ENV{CONVOS_REDIS_INDEX}) if $ENV{CONVOS_REDIS_INDEX} and !$url->path->[0];
398 8         344 $ENV{CONVOS_REDIS_URL} = $url->to_string;
399             }
400              
401             sub _set_secrets {
402 3     3   7 my $self = shift;
403 3         67 my $redis = $self->redis;
404              
405             $self->delay(
406             sub {
407 0     0   0 my ($delay) = @_;
408 0         0 $redis->lrange('convos:secrets', 0, -1, $delay->begin);
409 0         0 $redis->getset('convos:secrets:lock' => 1, $delay->begin);
410 0         0 $redis->expire('convos:secrets:lock' => 5);
411             },
412             sub {
413 0     0   0 my ($delay, $secrets, $locked) = @_;
414              
415 0   0     0 $secrets ||= $self->config->{secrets};
416              
417 0 0 0     0 return $self->app->secrets($secrets) if $secrets and @$secrets;
418 0 0       0 return $self->_set_secrets if $locked;
419 0         0 $secrets = [md5_sum rand . $$ . time];
420 0         0 $self->app->secrets($secrets);
421 0         0 $redis->lpush('convos:secrets', $secrets->[0]);
422 0         0 $redis->del('convos:secrets:lock');
423             },
424 0         0 );
425             }
426              
427             sub DESTROY {
428 14     14   245896 my $self = shift;
429 14         46 my $pid_file = $self->{pid_file};
430              
431 14 100 66     634 unlink $pid_file if $pid_file and -r $pid_file;
432             }
433              
434             =head1 COPYRIGHT AND LICENSE
435              
436             Copyright (C) 2012-2013, Nordaaker.
437              
438             This program is free software, you can redistribute it and/or modify it under
439             the terms of the Artistic License version 2.0.
440              
441             =head1 AUTHOR
442              
443             Jan Henning Thorsen - C
444              
445             Marcus Ramberg - C
446              
447             =cut
448              
449             1;