mirror of
https://github.com/dani/vroom.git
synced 2024-07-01 15:52:54 +02:00
![Daniel Berteaud](/assets/img/avatar_default.png)
And send the name of the participant in the notification email (only if name is asked before you can join)
903 lines
29 KiB
Perl
Executable File
903 lines
29 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# This file is part of the VROOM project
|
|
# released under the MIT licence
|
|
# Copyright 2014 Firewall Services
|
|
|
|
use lib '../lib';
|
|
use Mojolicious::Lite;
|
|
use Mojolicious::Plugin::Mailer;
|
|
use Mojo::JSON;
|
|
use DBI;
|
|
use Data::GUID qw(guid_string);
|
|
use Digest::MD5 qw(md5_hex);
|
|
use Crypt::SaltedHash;
|
|
use MIME::Base64;
|
|
use Email::Sender::Transport::Sendmail;
|
|
use Encode;
|
|
use File::stat;
|
|
|
|
# List The different components we rely on.
|
|
# Used to generate thanks on the about template
|
|
our $components = {
|
|
"SimpleWebRTC" => {
|
|
url => 'http://simplewebrtc.com/'
|
|
},
|
|
"Mojolicious" => {
|
|
url => 'http://mojolicio.us/'
|
|
},
|
|
"Jquery" => {
|
|
url => 'http://jquery.com/'
|
|
},
|
|
"notify.js" => {
|
|
url => 'http://notifyjs.com/'
|
|
},
|
|
"jquery-browser-plugin" => {
|
|
url => 'https://github.com/gabceb/jquery-browser-plugin'
|
|
},
|
|
"sprintf.js" => {
|
|
url => 'http://hexmen.com/blog/2007/03/printf-sprintf/'
|
|
},
|
|
"node.js" => {
|
|
url => 'http://nodejs.org/'
|
|
},
|
|
"bootstrap" => {
|
|
url => 'http://getbootstrap.com/'
|
|
},
|
|
"MariaDB" => {
|
|
url => 'https://mariadb.org/'
|
|
},
|
|
"SignalMaster" => {
|
|
url => 'https://github.com/andyet/signalmaster/'
|
|
},
|
|
"rfc5766-turn-server" => {
|
|
url => 'https://code.google.com/p/rfc5766-turn-server/'
|
|
},
|
|
"FileSaver" => {
|
|
url => 'https://github.com/eligrey/FileSaver.js'
|
|
}
|
|
};
|
|
|
|
app->log->level('info');
|
|
# Read conf file, and set default values
|
|
our $config = plugin Config => {
|
|
file => '../conf/vroom.conf',
|
|
default => {
|
|
dbi => 'DBI:mysql:database=vroom;host=localhost',
|
|
dbUser => 'vroom',
|
|
dbPassword => 'vroom',
|
|
signalingServer => 'https://vroom.example.com/',
|
|
stunServer => 'stun.l.google.com:19302',
|
|
realm => 'vroom',
|
|
emailFrom => 'vroom@example.com',
|
|
feedbackRecipient => 'admin@example.com',
|
|
poweredBy => '<a href="http://www.firewall-services.com" target="_blank">Firewall Services</a>',
|
|
template => 'default',
|
|
inactivityTimeout => 3600,
|
|
persistentInactivityTimeout => 0,
|
|
logLevel => 'info',
|
|
chromeExtensionId => 'ecicdpoejfllflombfanbhfpgcimjddn',
|
|
sendmail => '/sbin/sendmail'
|
|
}
|
|
};
|
|
|
|
app->log->level($config->{logLevel});
|
|
|
|
# Load I18N, and declare supported languages
|
|
plugin I18N => {
|
|
namespace => 'Vroom::I18N',
|
|
support_url_langs => [qw(en fr)]
|
|
};
|
|
|
|
# Load mailer plugin with its default values
|
|
plugin Mailer => {
|
|
from => $config->{emailFrom},
|
|
transport => Email::Sender::Transport::Sendmail->new({ sendmail => $config->{sendmail}}),
|
|
};
|
|
|
|
# Wrapper arround DBI
|
|
helper db => sub {
|
|
my $dbh = DBI->connect($config->{dbi}, $config->{dbUser}, $config->{dbPassword}) || die "Could not connect";
|
|
$dbh
|
|
};
|
|
|
|
# Create a cookie based session
|
|
helper login => sub {
|
|
my $self = shift;
|
|
return if $self->session('name');
|
|
my $login = $ENV{'REMOTE_USER'} || lc guid_string();
|
|
$self->session( name => $login,
|
|
ip => $self->tx->remote_address );
|
|
$self->app->log->info($self->session('name') . " logged in from " . $self->tx->remote_address);
|
|
};
|
|
|
|
# Expire the cookie
|
|
helper logout => sub {
|
|
my $self = shift;
|
|
$self->session( expires => 1 );
|
|
$self->app->log->info($self->session('name') . " logged out");
|
|
};
|
|
|
|
# Create a new room in the DB
|
|
# Requires two args: the name of the room and the session name of the creator
|
|
helper create_room => sub {
|
|
my $self = shift;
|
|
my ($name,$owner) = @_;
|
|
# Exit if the name isn't valid or already taken
|
|
return undef if ( $self->get_room($name) || !$self->valid_room_name($name));
|
|
my $sth = eval { $self->db->prepare("INSERT INTO rooms (name,create_timestamp,activity_timestamp,owner,token,realm) VALUES (?,?,?,?,?,?);") } || return undef;
|
|
# Gen a random token. Will be used as a turnPassword
|
|
my $tp = join '' => map{('a'..'z','A'..'Z','0'..'9')[rand 62]} 0..49;
|
|
$sth->execute($name,time(),time(),$owner,$tp,$config->{realm}) || return undef;
|
|
$self->app->log->info("room $name created by " . $self->session('name'));
|
|
return 1;
|
|
};
|
|
|
|
# Read room param in the DB and return a perl hash
|
|
helper get_room => sub {
|
|
my $self = shift;
|
|
my ($name) = @_;
|
|
my $sth = eval { $self->db->prepare("SELECT * from rooms where name=?;") } || return undef;
|
|
$sth->execute($name) || return undef;
|
|
return $sth->fetchall_hashref('name')->{$name};
|
|
};
|
|
|
|
# Lock/unlock a room, to prevent new participants
|
|
# Takes two arg: room name and 1 for lock, 0 for unlock
|
|
helper lock_room => sub {
|
|
my $self = shift;
|
|
my ($name,$lock) = @_;
|
|
return undef unless ( %{ $self->get_room($name) });
|
|
return undef unless ($lock =~ m/^0|1$/);
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET locked=? where name=?;") } || return undef;
|
|
$sth->execute($lock,$name) || return undef;
|
|
my $action = ($lock eq '1') ? 'locked':'unlocked';
|
|
$self->app->log->info("room $name $action by " . $self->session('name'));
|
|
return 1;
|
|
};
|
|
|
|
# Add a participant in the database. Used by the signaling server to check
|
|
# if user is allowed
|
|
helper add_participant => sub {
|
|
my $self = shift;
|
|
my ($name,$participant) = @_;
|
|
my $room = $self->get_room($name) || return undef;
|
|
my $sth = eval { $self->db->prepare("INSERT IGNORE INTO participants (id,participant) VALUES (?,?);") } || return undef;
|
|
$sth->execute($room->{id},$participant) || return undef;
|
|
$self->app->log->info($self->session('name') . " joined the room $name");
|
|
return 1;
|
|
};
|
|
|
|
# Remove participant from the DB
|
|
helper remove_participant => sub {
|
|
my $self = shift;
|
|
my ($name,$participant) = @_;
|
|
my $room = $self->get_room($name) || return undef;
|
|
my $sth = eval { $self->db->prepare("DELETE FROM participants WHERE id=? AND participant=?;") } || return undef;
|
|
$sth->execute($room->{id},$participant) || return undef;
|
|
$self->app->log->info($self->session('name') . " leaved the room $name");
|
|
return 1;
|
|
};
|
|
|
|
# Get a list of participants of a room
|
|
helper get_participants => sub {
|
|
my $self = shift;
|
|
my ($name) = @_;
|
|
my $room = $self->get_room($name) || return undef;
|
|
my $sth = eval { $self->db->prepare("SELECT participant FROM participants WHERE id=?;") } || return undef;
|
|
$sth->execute($room->{id}) || return undef;
|
|
my @res;
|
|
while(my @row = $sth->fetchrow_array){
|
|
push @res, $row[0];
|
|
}
|
|
return @res;
|
|
};
|
|
|
|
# Set the role of a peer
|
|
helper set_peer_role => sub {
|
|
my $self = shift;
|
|
my ($room,$name,$id,$role) = @_;
|
|
# Check if this ID isn't the one from another peer first
|
|
my $sth = eval { $self->db->prepare("SELECT * FROM participants WHERE peer_id=? AND participant!=? AND id IN (SELECT id FROM rooms WHERE name=?)") } || return undef;
|
|
$sth->execute($id,$name,$room) || return undef;
|
|
return undef if ($sth->rows > 0);
|
|
$sth = eval { $self->db->prepare("UPDATE participants SET peer_id=?,role=? WHERE participant=? AND id IN (SELECT id FROM rooms WHERE name=?)") } || return undef;
|
|
$sth->execute($id,$role,$name,$room) || return undef;
|
|
return 1;
|
|
};
|
|
|
|
# Return the role of a peer, from it's signaling ID
|
|
helper get_peer_role => sub {
|
|
my $self = shift;
|
|
my ($room,$id) = @_;
|
|
my $sth = eval { $self->db->prepare("SELECT role from participants WHERE peer_id=? AND id IN (SELECT id FROM rooms WHERE name=?)") } || return undef;
|
|
$sth->execute($id,$room) || return undef;
|
|
if ($sth->rows == 1){
|
|
my ($role) = $sth->fetchrow_array();
|
|
return $role;
|
|
}
|
|
else{
|
|
return 'participant';
|
|
}
|
|
};
|
|
|
|
# Check if a participant has joined a room
|
|
# Takes two args: the session name, and the room name
|
|
helper has_joined => sub {
|
|
my $self = shift;
|
|
my ($session,$name) = @_;
|
|
my $ret = 0;
|
|
my $sth = eval { $self->db->prepare("SELECT * FROM rooms WHERE name=? AND id IN (SELECT id FROM participants WHERE participant=?)") } || return undef;
|
|
$sth->execute($name,$session) || return undef;
|
|
$ret = 1 if ($sth->rows > 0);
|
|
return $ret;
|
|
};
|
|
|
|
# Purge unused rooms
|
|
helper delete_rooms => sub {
|
|
my $self = shift;
|
|
$self->app->log->debug('Removing unused rooms');
|
|
eval {
|
|
my $timeout = time()-$config->{inactivityTimeout};
|
|
$self->db->do("DELETE FROM participants WHERE id IN (SELECT id FROM rooms WHERE activity_timestamp < $timeout AND persistent='0');");
|
|
$self->db->do("DELETE FROM notifications WHERE id IN (SELECT id FROM rooms WHERE activity_timestamp < $timeout AND persistent='0');");
|
|
$self->db->do("DELETE FROM rooms WHERE activity_timestamp < $timeout AND persistent='0';");
|
|
} || return undef;
|
|
if ($config->{persistentInactivityTimeout} && $config->{persistentInactivityTimeout} > 0){
|
|
eval {
|
|
my $timeout = time()-$config->{persistentInactivityTimeout};
|
|
$self->db->do("DELETE FROM participants WHERE id IN (SELECT id FROM rooms WHERE activity_timestamp < $timeout AND persistent='1');");
|
|
$self->db->do("DELETE FROM notifications WHERE id IN (SELECT id FROM rooms WHERE activity_timestamp < $timeout AND persistent='1');");
|
|
$self->db->do("DELETE FROM rooms WHERE activity_timestamp < $timeout AND persistent='1';");
|
|
} || return undef;
|
|
}
|
|
return 1;
|
|
};
|
|
|
|
# Just update the activity timestamp
|
|
# so we can detect unused rooms
|
|
helper ping_room => sub {
|
|
my $self = shift;
|
|
my ($name) = @_;
|
|
return undef unless ( %{ $self->get_room($name) });
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET activity_timestamp=? where name=?;") } || return undef;
|
|
$sth->execute(time(),$name) || return undef;
|
|
$self->app->log->debug($self->session('name') . " pinged the room $name");
|
|
return 1;
|
|
};
|
|
|
|
# Check if this name is a valid room name
|
|
helper valid_room_name => sub {
|
|
my $self = shift;
|
|
my ($name) = @_;
|
|
my $ret = undef;
|
|
# A few names are reserved
|
|
my @reserved = qw(about help feedback goodbye admin create localize action missing dies password kicked);
|
|
if ($name =~ m/^[\w\-]{1,49}$/ && !grep { $name eq $_ } @reserved){
|
|
$ret = 1;
|
|
}
|
|
return $ret;
|
|
};
|
|
|
|
# Generate a random name
|
|
helper get_random_name => sub {
|
|
my $self = shift;
|
|
my $name = join '' => map{('a'..'z','0'..'9')[rand 36]} 0..9;
|
|
# Get another one if already taken
|
|
while ($self->get_room($name)){
|
|
$name = $self->get_random_name();
|
|
}
|
|
return $name;
|
|
};
|
|
|
|
# Return the mtime of a file
|
|
# Used to append the timestamp to JS and CSS files
|
|
# So client can get new version immediatly
|
|
helper get_mtime => sub {
|
|
my $self = shift;
|
|
my ($file) = @_;
|
|
return stat($file)->mtime;
|
|
};
|
|
|
|
# Wrapper arround url_for which adds a trailing / if needed
|
|
helper get_url => sub {
|
|
my $self = shift;
|
|
my $url = $self->url_for(shift);
|
|
$url .= ($url =~ m/\/$/) ? '' : '/';
|
|
return $url;
|
|
};
|
|
|
|
# Password protect a room
|
|
# Takes two args: room name and password
|
|
# If password is undef: remove the password
|
|
# Password is hashed and salted before being stored
|
|
helper set_join_pass => sub {
|
|
my $self = shift;
|
|
my ($room,$pass) = @_;
|
|
return undef unless ( %{ $self->get_room($room) });
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET join_password=? where name=?;") } || return undef;
|
|
$pass = ($pass) ? Crypt::SaltedHash->new(algorithm => 'SHA-256')->add($pass)->generate : undef;
|
|
$sth->execute($pass,$room) || return undef;
|
|
if ($pass){
|
|
$self->app->log->debug($self->session('name') . " has set a password on room $room");
|
|
}
|
|
else{
|
|
$self->app->log->debug($self->session('name') . " has removed password on room $room");
|
|
}
|
|
return 1;
|
|
};
|
|
|
|
# Set owner password. Not needed to join a room
|
|
# but needed to prove you're the owner, and access the configuration menu
|
|
helper set_owner_pass => sub {
|
|
my $self = shift;
|
|
my ($room,$pass) = @_;
|
|
return undef unless ( %{ $self->get_room($room) });
|
|
# For now, setting an owner password makes the room persistant
|
|
# Might be separated in the future
|
|
if ($pass){
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET owner_password=?,persistent='1' where name=?;") } || return undef;
|
|
my $pass = Crypt::SaltedHash->new(algorithm => 'SHA-256')->add($pass)->generate;
|
|
$sth->execute($pass,$room) || return undef;
|
|
$self->app->log->debug($self->session('name') . " has set an owner password on room $room, which is now persistent");
|
|
}
|
|
else{
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET owner_password=?,persistent='0' where name=?;") } || return undef;
|
|
$sth->execute(undef,$room) || return undef;
|
|
$self->app->log->debug($self->session('name') . " has removed the owner password on room $room, which is not persistent anymore");
|
|
}
|
|
};
|
|
|
|
# Add an email address to the list of notifications
|
|
helper add_notification => sub {
|
|
my $self = shift;
|
|
my ($room,$email) = @_;
|
|
my $data = $self->get_room($room);
|
|
return undef unless ($data);
|
|
my $sth = eval { $self->db->prepare("INSERT INTO notifications (id,email) VALUES (?,?)") } || return undef;
|
|
$sth->execute($data->{id},$email) || return undef;
|
|
return 1;
|
|
};
|
|
|
|
# Return the list of email addresses
|
|
helper get_notification => sub {
|
|
my $self = shift;
|
|
my ($room) = @_;
|
|
$room = $self->get_room($room) || return undef;
|
|
my $sth = eval { $self->db->prepare("SELECT email FROM notifications WHERE id=?;") } || return undef;
|
|
$sth->execute($room->{id}) || return undef;
|
|
my @res;
|
|
while(my @row = $sth->fetchrow_array){
|
|
push @res, $row[0];
|
|
}
|
|
return @res;
|
|
};
|
|
|
|
# Remove an email from notification list
|
|
helper remove_notification => sub {
|
|
my $self = shift;
|
|
my ($room,$email) = @_;
|
|
my $data = $self->get_room($room);
|
|
return undef unless ($data);
|
|
my $sth = eval { $self->db->prepare("DELETE FROM notifications where id=? and email=?") } || return undef;
|
|
$sth->execute($data->{id},$email) || return undef;
|
|
return 1;
|
|
};
|
|
|
|
|
|
# Set/unset ask for name
|
|
helper ask_for_name => sub {
|
|
my $self = shift;
|
|
my ($room,$set) = @_;
|
|
my $data = $self->get_room($room);
|
|
return undef unless ($data);
|
|
my $sth = eval { $self->db->prepare("UPDATE rooms SET ask_for_name=? WHERE name=?") } || return undef;
|
|
$sth->execute($set,$room) || return undef;
|
|
return 1;
|
|
};
|
|
|
|
# Route / to the index page
|
|
any '/' => 'index';
|
|
|
|
# Route for the about page
|
|
get '/about' => sub {
|
|
my $self = shift;
|
|
$self->stash( components => $components );
|
|
} => 'about';
|
|
|
|
# Route for the help page
|
|
get '/help' => 'help';
|
|
|
|
# Routes for feedback. One get to display the form
|
|
# and one post to get data from it
|
|
get '/feedback' => 'feedback';
|
|
post '/feedback' => sub {
|
|
my $self = shift;
|
|
my $email = $self->param('email') || '';
|
|
my $comment = $self->param('comment');
|
|
$self->email(
|
|
header => [
|
|
Subject => encode("MIME-Header", $self->l("FEEDBACK_FROM_VROOM")),
|
|
To => $config->{feedbackRecipient}
|
|
],
|
|
data => [
|
|
template => 'feedback',
|
|
email => $email,
|
|
comment => $comment
|
|
],
|
|
);
|
|
$self->redirect_to($self->get_url('feedback_thanks'));
|
|
};
|
|
|
|
# Route for the thanks after feedback form
|
|
get 'feedback_thanks' => 'feedback_thanks';
|
|
|
|
# Route for the goodbye page, displayed when someone leaves a room
|
|
get '/goodby/(:room)' => sub {
|
|
my $self = shift;
|
|
my $room = $self->stash('room');
|
|
if (!$self->get_room($room)){
|
|
return $self->render('error',
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
$self->remove_participant($room,$self->session('name'));
|
|
$self->logout;
|
|
} => 'goodby';
|
|
|
|
# Route for the kicked page
|
|
# Should be merged with the goodby route
|
|
get '/kicked/(:room)' => sub {
|
|
my $self = shift;
|
|
my $room = $self->stash('room');
|
|
if (!$self->get_room($room)){
|
|
return $self->render('error',
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
$self->remove_participant($room,$self->session('name'));
|
|
$self->logout;
|
|
} => 'kicked';
|
|
|
|
# This handler creates a new room
|
|
post '/create' => sub {
|
|
my $self = shift;
|
|
$self->res->headers->cache_control('max-age=1, no-cache');
|
|
# No name provided ? Lets generate one
|
|
my $name = $self->param('roomName') || $self->get_random_name();
|
|
# Create a session for this user, but don't set a role for now
|
|
$self->login;
|
|
# Error if the name is invalid
|
|
unless ($self->valid_room_name($name)){
|
|
return $self->render('error',
|
|
room => $name,
|
|
msg => $self->l('ERROR_NAME_INVALID'),
|
|
err => 'ERROR_NAME_INVALID'
|
|
);
|
|
}
|
|
# Cleanup unused rooms before trying to create it
|
|
$self->delete_rooms;
|
|
unless ($self->create_room($name,$self->session('name'))){
|
|
# If creation failed, it's most likly a name conflict
|
|
return $self->render('error',
|
|
room => $name,
|
|
msg => $self->l('ERROR_NAME_CONFLICT'),
|
|
err => 'ERROR_NAME_CONFLICT'
|
|
);
|
|
}
|
|
# Everything went fine, the room is created, lets mark this user owner of the room
|
|
# and redirect him on it.
|
|
else{
|
|
$self->session($name => {role => 'owner'});
|
|
$self->redirect_to($self->get_url('/') . $name);
|
|
}
|
|
};
|
|
|
|
# Translation for JS resources
|
|
# As there's no way to list all the available translated strings
|
|
# JS sends us the list it wants as a JSON object
|
|
# and we sent it back once localized
|
|
post '/localize' => sub {
|
|
my $self = shift;
|
|
my $strings = Mojo::JSON->new->decode($self->param('strings'));
|
|
foreach my $string (keys %$strings){
|
|
$strings->{$string} = $self->l($string);
|
|
}
|
|
return $self->render(json => $strings);
|
|
};
|
|
|
|
# Route for the password page
|
|
get '/password/(:room)' => sub {
|
|
my $self = shift;
|
|
my $room = $self->stash('room') || '';
|
|
my $data = $self->get_room($room);
|
|
unless ($data){
|
|
return $self->render('error',
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
$self->render('password', room => $room);
|
|
};
|
|
|
|
# Route for password submiting
|
|
post '/password/(:room)' => sub {
|
|
my $self = shift;
|
|
my $room = $self->stash('room') || '';
|
|
my $data = $self->get_room($room);
|
|
unless ($data){
|
|
return $self->render('error',
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
my $pass = $self->param('password');
|
|
# First check if we got the owner password, and if so, mark this user as owner
|
|
if ($data->{owner_password} && Crypt::SaltedHash->validate($data->{owner_password}, $pass)){
|
|
$self->session($room => {role => 'owner'});
|
|
$self->redirect_to($self->get_url('/') . $room);
|
|
}
|
|
# Then, check if it's the join password
|
|
elsif ($data->{join_password} && Crypt::SaltedHash->validate($data->{join_password}, $pass)){
|
|
$self->session($room => {role => 'participant'});
|
|
$self->redirect_to($self->get_url('/') . $room);
|
|
}
|
|
# Else, it's a wrong password, display an error page
|
|
else{
|
|
$self->render('error',
|
|
err => 'WRONG_PASSWORD',
|
|
msg => sprintf ($self->l("WRONG_PASSWORD"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
};
|
|
|
|
# Catch all route: if nothing else match, it's the name of a room
|
|
get '/(*room)' => sub {
|
|
my $self = shift;
|
|
my $room = $self->stash('room');
|
|
# Redirect to lower case
|
|
if ($room ne lc $room){
|
|
$self->redirect_to($self->get_url('/') . lc $room);
|
|
}
|
|
$self->delete_rooms;
|
|
unless ($self->valid_room_name($room)){
|
|
return $self->render('error',
|
|
msg => $self->l('ERROR_NAME_INVALID'),
|
|
err => 'ERROR_NAME_INVALID',
|
|
room => $room
|
|
);
|
|
}
|
|
my $data = $self->get_room($room);
|
|
unless ($data){
|
|
return $self->render('error',
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
room => $room
|
|
);
|
|
}
|
|
# Create a session if not already done
|
|
$self->login;
|
|
# If the room is locked and we're not the owner, we cannot join it !
|
|
if ($data->{'locked'} && (!$self->session($room) || !$self->session($room)->{role} || $self->session($room)->{role} ne 'owner')){
|
|
return $self->render('error',
|
|
msg => sprintf($self->l("ERROR_ROOM_s_LOCKED"), $room),
|
|
err => 'ERROR_ROOM_s_LOCKED',
|
|
room => $room,
|
|
ownerPass => ($data->{owner_password}) ? '1':'0'
|
|
);
|
|
}
|
|
# Now, if the room is password protected and we're not a participant, nor the owner, lets prompt for the password
|
|
if ($data->{join_password} && (!$self->session($room) || $self->session($room)->{role} !~ m/^participant|owner$/)){
|
|
return $self->redirect_to($self->get_url('/password') . $room);
|
|
}
|
|
# Set this peer as a simple participant if he has no role yet (shouldn't happen)
|
|
$self->session($room => {role => 'participant'}) if (!$self->session($room) || !$self->session($room)->{role});
|
|
# Short life cookie to negociate a session with the signaling server
|
|
$self->cookie(vroomsession => encode_base64($self->session('name') . ':' . $data->{name} . ':' . $data->{token}, ''), {expires => time + 60, path => '/'});
|
|
# Add this user to the participants table
|
|
unless($self->add_participant($room,$self->session('name'))){
|
|
return $self->render('error',
|
|
msg => $self->l('ERROR_OCCURED'),
|
|
err => 'ERROR_OCCURED',
|
|
room => $room
|
|
);
|
|
}
|
|
# Now display the room page
|
|
$self->render('join',
|
|
format => 'html',
|
|
turnPassword => $data->{token}
|
|
);
|
|
};
|
|
|
|
# Route for various room actions
|
|
post '/action' => sub {
|
|
my $self = shift;
|
|
my $action = $self->param('action');
|
|
my $room = $self->param('room') || "";
|
|
# Refuse any action from non members of the room
|
|
if (!$self->session('name') || !$self->has_joined($self->session('name'), $room) || !$self->session($room) || !$self->session($room)->{role}){
|
|
return $self->render(
|
|
json => {
|
|
msg => $self->l('ERROR_NOT_LOGGED_IN'),
|
|
status => 'error'
|
|
},
|
|
);
|
|
}
|
|
# Sanity check on the room name
|
|
return $self->render(
|
|
json => {
|
|
msg => sprintf ($self->l("ERROR_NAME_INVALID"), $room),
|
|
status => 'error'
|
|
},
|
|
) unless ($self->valid_room_name($room));
|
|
# Push the room name to the stash, just in case
|
|
$self->stash(room => $room);
|
|
# Gather room info from the DB
|
|
my $data = $self->get_room($room);
|
|
# Stop here if the room doesn't exist
|
|
return $self->render(
|
|
json => {
|
|
msg => sprintf ($self->l("ERROR_ROOM_s_DOESNT_EXIST"), $room),
|
|
err => 'ERROR_ROOM_s_DOESNT_EXIST',
|
|
status => 'error'
|
|
},
|
|
) unless ($data);
|
|
|
|
# Handle email invitation
|
|
if ($action eq 'invite'){
|
|
my $rcpt = $self->param('recipient');
|
|
my $message = $self->param('message');
|
|
my $status = 'error';
|
|
my $msg = $self->l('ERROR_OCCURED');
|
|
if ($rcpt !~ m/\S+@\S+\.\S+$/){
|
|
$msg = $self->l('ERROR_MAIL_INVALID');
|
|
}
|
|
elsif ($self->email(
|
|
header => [
|
|
Subject => encode("MIME-Header", $self->l("EMAIL_INVITATION")),
|
|
To => $rcpt
|
|
],
|
|
data => [
|
|
template => 'invite',
|
|
room => $room,
|
|
message => $message
|
|
],
|
|
)){
|
|
$self->app->log->info($self->session('name') . " sent an invitation for room $room to $rcpt");
|
|
$status = 'success';
|
|
$msg = sprintf($self->l('INVITE_SENT_TO_s'), $rcpt);
|
|
}
|
|
$self->render(
|
|
json => {
|
|
msg => $msg,
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# Handle room lock/unlock
|
|
if ($action =~ m/(un)?lock/){
|
|
my ($lock,$success);
|
|
my $msg = 'ERROR_OCCURED';
|
|
my $status = 'error';
|
|
# Only the owner can lock or unlock a room
|
|
if (!$self->session($room) || $self->session($room)->{role} ne 'owner'){
|
|
$msg = 'NOT_ALLOWED';
|
|
}
|
|
elsif ($self->lock_room($room,($action eq 'lock') ? '1':'0')){
|
|
$status = 'success';
|
|
$msg = ($action eq 'lock') ? 'ROOM_LOCKED' : 'ROOM_UNLOCKED';
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $self->l($msg),
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# Handle activity pings sent every minute by each participant
|
|
elsif ($action eq 'ping'){
|
|
my $status = 'error';
|
|
my $msg = 'ERROR_OCCURED';
|
|
my $res = $self->ping_room($room);
|
|
# Cleanup expired rooms every ~10 pings
|
|
if ((int (rand 100)) <= 10){
|
|
$self->delete_rooms;
|
|
}
|
|
if ($res){
|
|
$status = 'success';
|
|
$msg = '';
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $self->l($msg),
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# Handle password (join and owner)
|
|
elsif ($action eq 'setPassword'){
|
|
my $pass = $self->param('password');
|
|
my $type = $self->param('type') || 'join';
|
|
# Empty password is equivalent to no password at all
|
|
$pass = undef if ($pass && $pass eq '');
|
|
my $res = undef;
|
|
my $msg = 'ERROR_OCCURED';
|
|
my $status = 'error';
|
|
# Once again, only the owner can do this
|
|
if ($self->session($room)->{role} eq 'owner'){
|
|
if ($type eq 'owner'){
|
|
$res = $self->set_owner_pass($room,$pass);
|
|
}
|
|
else{
|
|
$res = $self->set_join_pass($room,$pass);
|
|
}
|
|
if ($res){
|
|
$msg = ($pass) ? $self->l('PASSWORD_SET') : $self->l('PASSWORD_REMOVED');
|
|
$status = 'success';
|
|
}
|
|
}
|
|
# Simple participants will get an error
|
|
else{
|
|
$msg = 'NOT_ALLOWED';
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $msg,
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# A participant is trying to auth as an owner, lets check that
|
|
elsif ($action eq 'authenticate'){
|
|
my $pass = $self->param('password');
|
|
my $res = undef;
|
|
my $msg = 'ERROR_OCCURED';
|
|
my $status = 'error';
|
|
# Auth succeed ? lets promote him to owner of the room
|
|
if ($data->{owner_password} && Crypt::SaltedHash->validate($data->{owner_password}, $pass)){
|
|
$self->session($room, {role => 'owner'});
|
|
$msg = 'AUTH_SUCCESS';
|
|
$status = 'success';
|
|
}
|
|
elsif ($data->{owner_password}){
|
|
$msg = 'WRONG_PASSWORD';
|
|
}
|
|
# There's no owner password, so you cannot auth
|
|
else{
|
|
$msg = 'NOT_ALLOWED';
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $self->l($msg),
|
|
status => $status
|
|
},
|
|
);
|
|
}
|
|
# Return your role and various info about the room
|
|
elsif ($action eq 'getRoomInfo'){
|
|
my $id = $self->param('id');
|
|
my $res = 'error';
|
|
my %emailNotif;
|
|
if ($self->session($room) && $self->session($room)->{role}){
|
|
$res = ($self->set_peer_role($room,$self->session('name'),$id, $self->session($room)->{role})) ? 'success':$res;
|
|
}
|
|
if ($self->session($room)->{role} eq 'owner'){
|
|
my $i = 0;
|
|
my @email = $self->get_notification($room);
|
|
%emailNotif = map { $i => $email[$i++] } @email;
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
role => $self->session($room)->{role},
|
|
owner_auth => ($data->{owner_password}) ? 'yes' : 'no',
|
|
join_auth => ($data->{join_password}) ? 'yes' : 'no',
|
|
locked => ($data->{locked}) ? 'yes' : 'no',
|
|
ask_for_name => ($data->{ask_for_name}) ? 'yes' : 'no',
|
|
notif => Mojo::JSON->new->encode({email => { %emailNotif }}),
|
|
status => $res
|
|
},
|
|
);
|
|
}
|
|
# Return the role of a peer
|
|
elsif ($action eq 'getPeerRole'){
|
|
my $id = $self->param('id');
|
|
my $role = $self->get_peer_role($room,$id);
|
|
return $self->render(
|
|
json => {
|
|
role => $role,
|
|
status => 'success'
|
|
}
|
|
);
|
|
}
|
|
# Add a new email for notifications when someone joins
|
|
elsif ($action eq 'emailNotification'){
|
|
my $email = $self->param('email');
|
|
my $type = $self->param('type');
|
|
my $status = 'error';
|
|
my $msg = $self->l('ERROR_OCCURED');
|
|
if ($self->session($room)->{role} ne 'owner'){
|
|
$msg = $self->l('NOT_ALLOWED');
|
|
}
|
|
elsif ($email !~ m/^\S+@\S+\.\S+$/){
|
|
$msg = $self->l('ERROR_MAIL_INVALID');
|
|
}
|
|
elsif ($type eq 'add' && $self->add_notification($room,$email)){
|
|
$status = 'success';
|
|
$msg = sprintf($self->l('s_WILL_BE_NOTIFIED'), $email);
|
|
}
|
|
elsif ($type eq 'remove' && $self->remove_notification($room,$email)){
|
|
$status = 'success';
|
|
$msg = sprintf($self->l('s_WONT_BE_NOTIFIED_ANYMORE'), $email);
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $msg,
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# Set/unset askForName
|
|
elsif ($action eq 'askForName'){
|
|
my $type = $self->param('type');
|
|
my $status = 'error';
|
|
my $msg = $self->l('ERROR_OCCURED');
|
|
if ($self->session($room)->{role} ne 'owner'){
|
|
$msg = $self->l('NOT_ALLOWED');
|
|
}
|
|
elsif($type eq 'set' && $self->ask_for_name($room,'1')){
|
|
$status = 'success';
|
|
$msg = $self->l('FORCE_DISPLAY_NAME');
|
|
}
|
|
elsif($type eq 'unset' && $self->ask_for_name($room,'0')){
|
|
$status = 'success';
|
|
$msg = $self->l('NAME_WONT_BE_ASKED');
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
msg => $msg,
|
|
status => $status
|
|
}
|
|
);
|
|
}
|
|
# New participant joined the room
|
|
elsif ($action eq 'join'){
|
|
my $name = $self->param('name') || '';
|
|
# Send notifications
|
|
foreach my $rcpt ($self->get_notification($room)){
|
|
$self->email(
|
|
header => [
|
|
Subject => encode("MIME-Header", $self->l("JOIN_NOTIFICATION")),
|
|
To => $rcpt
|
|
],
|
|
data => [
|
|
template => 'notification',
|
|
room => $room,
|
|
name => $name
|
|
],
|
|
);
|
|
}
|
|
return $self->render(
|
|
json => {
|
|
status => 'success'
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
# use the templates defined in the config
|
|
push @{app->renderer->paths}, '../templates/'.$config->{template};
|
|
# Set the secret used to sign cookies
|
|
app->secret($config->{secret});
|
|
app->sessions->secure(1);
|
|
app->sessions->cookie_name('vroom');
|
|
# And start, lets VROOM !!
|
|
app->start;
|
|
|