File Coverage

blib/lib/Convos.pm
Criterion Covered Total %
statement 134 158 84.8
branch 33 52 63.4
condition 14 33 42.4
subroutine 19 22 86.3
pod 1 1 100.0
total 201 266 75.5


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.8603
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   8497548 use Mojo::Base 'Mojolicious';
  36         72  
  36         212  
146 36     36   3519996 use Mojo::Redis;
  36         72692  
  36         261  
147 36     36   1181 use Mojo::Util qw( md5_sum );
  36         56  
  36         2079  
148 36     36   211 use File::Spec::Functions qw( catdir catfile tmpdir );
  36         51  
  36         1923  
149 36     36   166 use File::Basename qw( dirname );
  36         44  
  36         1447  
150 36     36   15809 use Convos::Core;
  36         100  
  36         358  
151 36     36   1060 use Convos::Core::Util ();
  36         62  
  36         64728  
152              
153             our $VERSION = '0.8603';
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 593932 my $self = shift;
190 45         92 my $config;
191              
192 45         187 $self->{convos_executable_path} = $0; # required to work from within toadfarm
193 45         225 $self->_from_cpan;
194 45         2739 $config = $self->_config;
195              
196 45 50       161 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         1018 $self->ua->max_redirects(2); # support getting facebook pictures
203 45         6061 $self->plugin('Convos::Plugin::Helpers');
204 45         2985 $self->plugin('LinkEmbedder');
205 45         172605 $self->secrets([time]); # will be replaced by _set_secrets()
206 45         439 $self->_redis_url;
207              
208 45 100       2653 return if $ENV{CONVOS_BACKEND_ONLY}; # set script/convos when started as backend
209              
210             # frontend code
211 43         989 $self->sessions->default_expiration(86400 * 30);
212 43 100       2101 $self->sessions->secure(1) if $ENV{CONVOS_SECURE_COOKIES};
213 43         208 $self->_assets;
214 43         573919 $self->_public_routes;
215 43         214 $self->_private_routes;
216              
217 43 50 66     14435 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       195 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         4 push @{$self->renderer->paths}, $ENV{CONVOS_TEMPLATES};
  1         39  
228             }
229              
230 43         503 $self->defaults(full_page => 1, organization_name => $self->config('name'));
231 43         1791 $self->hook(before_dispatch => \&_before_dispatch);
232 43 100       1983 $self->_embed_backend if $ENV{CONVOS_BACKEND_EMBEDDED};
233              
234 42         222 Scalar::Util::weaken($self);
235 42     3   866 Mojo::IOLoop->timer(0 => sub { $self->_set_secrets });
  3         21794  
236             }
237              
238             sub _assets {
239 43     43   80 my $self = shift;
240              
241 43         177 $self->plugin('AssetPack');
242 43         655608 $self->plugin('FontAwesome4', css => []);
243 43         66525 $self->asset('c.css' => qw( /scss/font-awesome.scss /sass/convos.scss ));
244 43         469624 $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   141882 my $c = shift;
269              
270 4   33     37 $c->stash(full_page => !($c->req->is_xhr || $c->param('_pjax')));
271              
272 4 50       2211 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       419 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   93 my $self = shift;
282 45 50       471 my $config = $ENV{MOJO_CONFIG} ? $self->plugin('Config') : $self->config;
283              
284 45   100     1105 $config->{hypnotoad}{listen} ||= [split /,/, $ENV{MOJO_LISTEN} || 'http://*:8080'];
      50        
285 45 50       182 $config->{hypnotoad}{pid_file} = $ENV{CONVOS_FRONTEND_PID_FILE} if $ENV{CONVOS_FRONTEND_PID_FILE};
286 45 50       167 $config->{hypnotoad}{group} = $ENV{RUN_AS_GROUP} if $ENV{RUN_AS_GROUP};
287 45 50       166 $config->{hypnotoad}{user} = $ENV{RUN_AS_USER} if $ENV{RUN_AS_USER};
288 45 100       154 $config->{name} = $ENV{CONVOS_ORGANIZATION_NAME} if $ENV{CONVOS_ORGANIZATION_NAME};
289 45   100     298 $config->{name} ||= 'Nordaaker';
290 45         87 $config;
291             }
292              
293             sub _embed_backend {
294 3     3   1737 my $self = shift;
295              
296 3 100       49 die "Cannot start embedded backend from hypnotoad" if $SIG{USR2};
297 1         751 require Convos::Control::Backend;
298 1         10 my $backend = Convos::Control::Backend->new;
299              
300 1         13 $backend->read_pid;
301              
302 1 50 33     25 if ($backend->pid and $backend->pid_running) {
303 0         0 $self->app->log->warn('Backend is already running.');
304             }
305             else {
306 1         13 $backend->pid($$);
307 1         17 $backend->write_pid;
308 1         444 $self->{pid_file} = $backend->pid_file;
309 1         46 $self->log->info('Starting convos backend.');
310 1         522 $self->core->start;
311             }
312             }
313              
314             sub _from_cpan {
315 45     45   82 my $self = shift;
316 45         2691 my $home = catdir dirname(__FILE__), 'Convos';
317              
318 45 50       909 return if -d $self->home->rel_dir('templates');
319 45         3190 $self->home->parse($home);
320 45         2211 $self->static->paths->[0] = $self->home->rel_dir('public');
321 45         3469 $self->renderer->paths->[0] = $self->home->rel_dir('templates');
322             }
323              
324             sub _private_routes {
325 43     43   81 my $self = shift;
326 43         839 my $r = $self->routes->under('/')->to('user#auth', layout => 'view');
327              
328 43         13930 $r->websocket('/socket')->to('chat#socket')->name('socket');
329 43         21499 $r->get('/chat/command-history')->to('client#command_history');
330 43         24442 $r->get('/chat/notifications')->to('client#notifications', layout => undef)->name('notification.list');
331 43         24033 $r->post('/chat/notifications/clear')->to('client#clear_notifications', layout => undef)->name('notifications.clear');
332 43         25599 $r->any('/connection/add')->to('connection#add_connection')->name('connection.add');
333 43         22539 $r->any('/connection/:name/control')->to('connection#control')->name('connection.control');
334 43         24670 $r->any('/connection/:name/edit')->to('connection#edit_connection')->name('connection.edit');
335 43         23829 $r->get('/connection/:name/delete')->to(template => 'connection/delete_connection', layout => 'tactile');
336 43         24805 $r->post('/connection/:name/delete')->to('connection#delete_connection')->name('connection.delete');
337 43         24787 $r->get('/oembed')->to('oembed#generate', layout => undef)->name('oembed');
338 43         21099 $r->any('/profile')->to('user#edit')->name('user.edit');
339 43         21572 $r->any('/profile/delete')->to('user#delete')->name('user.delete');
340 43         22842 $r->post('/profile/timezone/offset')->to('user#tz_offset');
341 43         24897 $r->get('/wizard')->to('connection#wizard')->name('wizard');
342              
343 43         21679 my $network_r = $r->any('/:network');
344 43         19212 $network_r->get('/*target' => [target => qr/[\#\&][^\x07\x2C\s]{1,50}/])->to('client#conversation', is_channel => 1)
345             ->name('view');
346 43         22921 $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         22569 $network_r->get('/')->to('client#conversation')->name('view.network');
349             }
350              
351             sub _public_routes {
352 43     43   167 my $self = shift;
353 43         1188 my $r = $self->routes->any->to(layout => 'tactile');
354              
355 43         20290 $r->get('/')->to('client#route')->name('index');
356 43     0   15889 $r->get('/convos')->to(cb => sub { shift->redirect_to('index'); });
  0         0  
357 43         23289 $r->get('/avatar')->to('user#avatar')->name('avatar');
358 43         21772 $r->get('/login')->to('user#login')->name('login');
359 43         21470 $r->post('/login')->to('user#login');
360 43         21516 $r->get('/register/:invite', {invite => ''})->to('user#register')->name('register');
361 43         23639 $r->post('/register/:invite', {invite => ''})->to('user#register');
362 43         25128 $r->get('/logout')->to('user#logout')->name('logout');
363 43         21690 $r;
364             }
365              
366             sub _redis_url {
367 45     45   86 my $self = shift;
368 45         73 my $url;
369              
370 45         153 for my $k (qw( CONVOS_REDIS_URL REDISTOGO_URL REDISCLOUD_URL DOTCLOUD_DATA_REDIS_URL )) {
371 166 100       538 $url = $ENV{$k} or next;
372 7         137 $self->log->debug("Using $k environment variable as Redis connection URL.");
373 7         1315 last;
374             }
375              
376 45 100       155 unless ($url) {
377 38 50       263 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         44 $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         2109 $self->log->debug("Could not find CONVOS_REDIS_URL value.");
387 37         1873 return;
388             }
389             }
390              
391 8         96 $url = Mojo::URL->new($url);
392 8 100 100     2902 $url->path($ENV{CONVOS_REDIS_INDEX}) if $ENV{CONVOS_REDIS_INDEX} and !$url->path->[0];
393 8         331 $ENV{CONVOS_REDIS_URL} = $url->to_string;
394             }
395              
396             sub _set_secrets {
397 3     3   7 my $self = shift;
398 3         47 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   184185 my $self = shift;
424 14         36 my $pid_file = $self->{pid_file};
425              
426 14 100 66     1683 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;