File Coverage

blib/lib/Convos.pm
Criterion Covered Total %
statement 126 158 79.7
branch 31 52 59.6
condition 12 33 36.3
subroutine 19 22 86.3
pod 1 1 100.0
total 189 266 71.0


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.8602
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   8543782 use Mojo::Base 'Mojolicious';
  36         68  
  36         234  
146 36     36   3515243 use Mojo::Redis;
  36         71367  
  36         267  
147 36     36   885 use Mojo::Util qw( md5_sum );
  36         54  
  36         1943  
148 36     36   187 use File::Spec::Functions qw( catdir catfile tmpdir );
  36         57  
  36         1910  
149 36     36   159 use File::Basename qw( dirname );
  36         55  
  36         1453  
150 36     36   15488 use Convos::Core;
  36         87  
  36         342  
151 36     36   1022 use Convos::Core::Util ();
  36         49  
  36         63778  
152              
153             our $VERSION = '0.8602';
154              
155             $ENV{CONVOS_DEFAULT_CONNECTION} //= 'chat.freenode.net:6697';
156              
157             =head1 ATTRIBUTES
158              
159             =head2 core
160              
161             Holds a L object .
162              
163             =head2 upgrader
164              
165             DEPRECATED.
166              
167             =cut
168              
169             has core => sub {
170             my $self = shift;
171             my $core = Convos::Core->new(redis => $self->redis);
172              
173             $core->log($self->log);
174             $core->archive->log_dir($ENV{CONVOS_ARCHIVE_DIR} || $self->home->rel_dir('irc_logs'));
175             $core;
176             };
177              
178             has upgrader => sub { die "upgrader() is deprecated" };
179              
180             =head1 METHODS
181              
182             =head2 startup
183              
184             This method will run once at server start
185              
186             =cut
187              
188             sub startup {
189 45     45 1 599592 my $self = shift;
190 45         94 my $config;
191              
192 45         192 $self->{convos_executable_path} = $0; # required to work from within toadfarm
193 45         190 $self->_from_cpan;
194 45         2893 $config = $self->_config;
195              
196 45 50       155 if (my $log = $config->{log}) {
197 0 0       0 $self->log->level($log->{level}) if $log->{level};
198 0 0 0     0 $self->log->path($log->{file}) if $log->{file} ||= $log->{path} || $ENV{CONVOS_FRONTEND_LOGFILE};
      0        
199 0         0 delete $self->log->{handle}; # make sure it's fresh to file
200             }
201              
202 45         1014 $self->ua->max_redirects(2); # support getting facebook pictures
203 45         6081 $self->plugin('Convos::Plugin::Helpers');
204 45         3109 $self->plugin('LinkEmbedder');
205 45         175413 $self->secrets([time]); # will be replaced by _set_secrets()
206 45         434 $self->_redis_url;
207              
208 45 100       2432 return if $ENV{CONVOS_BACKEND_ONLY}; # set script/convos when started as backend
209              
210             # frontend code
211 43         973 $self->sessions->default_expiration(86400 * 30);
212 43 100       2081 $self->sessions->secure(1) if $ENV{CONVOS_SECURE_COOKIES};
213 43         202 $self->_assets;
214 43         529492 $self->_public_routes;
215 43         182 $self->_private_routes;
216              
217 43 50 66     13107 if (!$ENV{CONVOS_INVITE_CODE} and $config->{invite_code}) {
218 0         0 $self->log->warn(
219             "invite_code from config file will be deprecated. Set the CONVOS_INVITE_CODE env variable instead.");
220 0         0 $ENV{CONVOS_INVITE_CODE} = $config->{invite_code};
221             }
222 43 100       172 if ($ENV{CONVOS_TEMPLATES}) {
223              
224             # Using push() since I don't think it's a good idea for allowing the user
225             # to customize every template, at least not when the application is still
226             # unstable.
227 1         2 push @{$self->renderer->paths}, $ENV{CONVOS_TEMPLATES};
  1         21  
228             }
229              
230 43         303 $self->defaults(full_page => 1, organization_name => $self->config('name'));
231 43         1449 $self->hook(before_dispatch => \&_before_dispatch);
232 43 100       1685 $self->_embed_backend if $ENV{CONVOS_BACKEND_EMBEDDED};
233              
234 41         155 Scalar::Util::weaken($self);
235 41     3   577 Mojo::IOLoop->timer(0 => sub { $self->_set_secrets });
  3         17661  
236             }
237              
238             sub _assets {
239 43     43   76 my $self = shift;
240              
241 43         151 $self->plugin('AssetPack');
242 43         661670 $self->plugin('FontAwesome4', css => []);
243 43         65078 $self->asset('c.css' => qw( /scss/font-awesome.scss /sass/convos.scss ));
244 43         199505 $self->asset(
245             'c.js' => qw(
246             https://platform.twitter.com/widgets.js
247             /js/globals.js
248             /js/jquery.js
249             /js/ws-reconnecting.js
250             /js/jquery.hotkeys.js
251             /js/jquery.finger.js
252             /js/jquery.pjax.js
253             /js/jquery.notify.js
254             /js/jquery.disableouterscroll.js
255             /js/convos.events.js
256             /js/convos.sidebar.js
257             /js/convos.socket.js
258             /js/convos.input.js
259             /js/convos.conversations.js
260             /js/convos.nicks.js
261             /js/convos.goto-anything.js
262             /js/convos.chat.js
263             )
264             );
265             }
266              
267             sub _before_dispatch {
268 4     4   131333 my $c = shift;
269              
270 4   33     16 $c->stash(full_page => !($c->req->is_xhr || $c->param('_pjax')));
271              
272 4 50       1974 if (my $base = $c->req->headers->header('X-Request-Base')) {
273 0         0 $c->req->url->base(Mojo::URL->new($base));
274             }
275 4 50       457 if (!$c->app->config->{hostname_is_set}++) {
276 0         0 $c->redis->set('convos:frontend:url' => $c->req->url->base->to_abs->to_string);
277             }
278             }
279              
280             sub _config {
281 45     45   90 my $self = shift;
282 45 50       467 my $config = $ENV{MOJO_CONFIG} ? $self->plugin('Config') : $self->config;
283              
284 45   100     1075 $config->{hypnotoad}{listen} ||= [split /,/, $ENV{MOJO_LISTEN} || 'http://*:8080'];
      50        
285 45 50       171 $config->{hypnotoad}{pid_file} = $ENV{CONVOS_FRONTEND_PID_FILE} if $ENV{CONVOS_FRONTEND_PID_FILE};
286 45 50       160 $config->{hypnotoad}{group} = $ENV{RUN_AS_GROUP} if $ENV{RUN_AS_GROUP};
287 45 50       152 $config->{hypnotoad}{user} = $ENV{RUN_AS_USER} if $ENV{RUN_AS_USER};
288 45 100       153 $config->{name} = $ENV{CONVOS_ORGANIZATION_NAME} if $ENV{CONVOS_ORGANIZATION_NAME};
289 45   100     328 $config->{name} ||= 'Nordaaker';
290 45         110 $config;
291             }
292              
293             sub _embed_backend {
294 3     3   1287 my $self = shift;
295              
296 3 100       30 die "Cannot start embedded backend from hypnotoad" if $SIG{USR2};
297 1         532 require Convos::Control::Backend;
298 0         0 my $backend = Convos::Control::Backend->new;
299              
300 0         0 $backend->read_pid;
301              
302 0 0 0     0 if ($backend->pid and $backend->pid_running) {
303 0         0 $self->app->log->warn('Backend is already running.');
304             }
305             else {
306 0         0 $backend->pid($$);
307 0         0 $backend->write_pid;
308 0         0 $self->{pid_file} = $backend->pid_file;
309 0         0 $self->log->info('Starting convos backend.');
310 0         0 $self->core->start;
311             }
312             }
313              
314             sub _from_cpan {
315 45     45   79 my $self = shift;
316 45         2599 my $home = catdir dirname(__FILE__), 'Convos';
317              
318 45 50       970 return if -d $self->home->rel_dir('templates');
319 45         3282 $self->home->parse($home);
320 45         2415 $self->static->paths->[0] = $self->home->rel_dir('public');
321 45         3764 $self->renderer->paths->[0] = $self->home->rel_dir('templates');
322             }
323              
324             sub _private_routes {
325 43     43   98 my $self = shift;
326 43         732 my $r = $self->routes->under('/')->to('user#auth', layout => 'view');
327              
328 43         12799 $r->websocket('/socket')->to('chat#socket')->name('socket');
329 43         20469 $r->get('/chat/command-history')->to('client#command_history');
330 43         22013 $r->get('/chat/notifications')->to('client#notifications', layout => undef)->name('notification.list');
331 43         22109 $r->post('/chat/notifications/clear')->to('client#clear_notifications', layout => undef)->name('notifications.clear');
332 43         22838 $r->any('/connection/add')->to('connection#add_connection')->name('connection.add');
333 43         21342 $r->any('/connection/:name/control')->to('connection#control')->name('connection.control');
334 43         22656 $r->any('/connection/:name/edit')->to('connection#edit_connection')->name('connection.edit');
335 43         22318 $r->get('/connection/:name/delete')->to(template => 'connection/delete_connection', layout => 'tactile');
336 43         22170 $r->post('/connection/:name/delete')->to('connection#delete_connection')->name('connection.delete');
337 43         22720 $r->get('/oembed')->to('oembed#generate', layout => undef)->name('oembed');
338 43         20211 $r->any('/profile')->to('user#edit')->name('user.edit');
339 43         19992 $r->any('/profile/delete')->to('user#delete')->name('user.delete');
340 43         21064 $r->post('/profile/timezone/offset')->to('user#tz_offset');
341 43         22955 $r->get('/wizard')->to('connection#wizard')->name('wizard');
342              
343 43         19400 my $network_r = $r->any('/:network');
344 43         18406 $network_r->get('/*target' => [target => qr/[\#\&][^\x07\x2C\s]{1,50}/])->to('client#conversation', is_channel => 1)
345             ->name('view');
346 43         20609 $network_r->get('/*target' => [target => qr/[A-Za-z_\-\[\]\\\^\{\}\|\`][A-Za-z0-9_\-\[\]\\\^\{\}\|\`]{1,15}/])
347             ->to('client#conversation', is_channel => 0)->name('view');
348 43         20417 $network_r->get('/')->to('client#conversation')->name('view.network');
349             }
350              
351             sub _public_routes {
352 43     43   109 my $self = shift;
353 43         850 my $r = $self->routes->any->to(layout => 'tactile');
354              
355 43         16565 $r->get('/')->to('client#route')->name('index');
356 43     0   14044 $r->get('/convos')->to(cb => sub { shift->redirect_to('index'); });
  0         0  
357 43         21947 $r->get('/avatar')->to('user#avatar')->name('avatar');
358 43         19884 $r->get('/login')->to('user#login')->name('login');
359 43         19750 $r->post('/login')->to('user#login');
360 43         19368 $r->get('/register/:invite', {invite => ''})->to('user#register')->name('register');
361 43         22466 $r->post('/register/:invite', {invite => ''})->to('user#register');
362 43         22678 $r->get('/logout')->to('user#logout')->name('logout');
363 43         20188 $r;
364             }
365              
366             sub _redis_url {
367 45     45   91 my $self = shift;
368 45         75 my $url;
369              
370 45         127 for my $k (qw( CONVOS_REDIS_URL REDISTOGO_URL REDISCLOUD_URL DOTCLOUD_DATA_REDIS_URL )) {
371 166 100       510 $url = $ENV{$k} or next;
372 7         123 $self->log->debug("Using $k environment variable as Redis connection URL.");
373 7         1381 last;
374             }
375              
376 45 100       164 unless ($url) {
377 38 50       221 if ($self->config('redis')) {
    100          
378 0         0 $self->log->warn("'redis' url from config file will be deprecated. Run 'perldoc Convos' for alternative setup.");
379 0         0 $url = $self->config('redis');
380             }
381             elsif ($self->mode eq 'production') {
382 1         41 $self->log->debug("Using default Redis connection URL redis://127.0.0.1:6379/1");
383 1         34 $url = 'redis://127.0.0.1:6379/1';
384             }
385             else {
386 37         2213 $self->log->debug("Could not find CONVOS_REDIS_URL value.");
387 37         1847 return;
388             }
389             }
390              
391 8         63 $url = Mojo::URL->new($url);
392 8 100 100     2394 $url->path($ENV{CONVOS_REDIS_INDEX}) if $ENV{CONVOS_REDIS_INDEX} and !$url->path->[0];
393 8         304 $ENV{CONVOS_REDIS_URL} = $url->to_string;
394             }
395              
396             sub _set_secrets {
397 3     3   6 my $self = shift;
398 3         39 my $redis = $self->redis;
399              
400             $self->delay(
401             sub {
402 0     0   0 my ($delay) = @_;
403 0         0 $redis->lrange('convos:secrets', 0, -1, $delay->begin);
404 0         0 $redis->getset('convos:secrets:lock' => 1, $delay->begin);
405 0         0 $redis->expire('convos:secrets:lock' => 5);
406             },
407             sub {
408 0     0   0 my ($delay, $secrets, $locked) = @_;
409              
410 0   0     0 $secrets ||= $self->config->{secrets};
411              
412 0 0 0     0 return $self->app->secrets($secrets) if $secrets and @$secrets;
413 0 0       0 return $self->_set_secrets if $locked;
414 0         0 $secrets = [md5_sum rand . $$ . time];
415 0         0 $self->app->secrets($secrets);
416 0         0 $redis->lpush('convos:secrets', $secrets->[0]);
417 0         0 $redis->del('convos:secrets:lock');
418             },
419 0         0 );
420             }
421              
422             sub DESTROY {
423 14     14   182859 my $self = shift;
424 14         43 my $pid_file = $self->{pid_file};
425              
426 14 50 33     209 unlink $pid_file if $pid_file and -r $pid_file;
427             }
428              
429             =head1 COPYRIGHT AND LICENSE
430              
431             Copyright (C) 2012-2013, Nordaaker.
432              
433             This program is free software, you can redistribute it and/or modify it under
434             the terms of the Artistic License version 2.0.
435              
436             =head1 AUTHOR
437              
438             Jan Henning Thorsen - C
439              
440             Marcus Ramberg - C
441              
442             =cut
443              
444             1;