package CF::Controller::Websockets; use Mojo::Base 'Mojolicious::Controller'; use Mojo::Pg::PubSub; use Digest::SHA qw(hmac_sha1_hex); use POSIX qw(ceil); use feature 'signatures'; no warnings qw{ experimental::signatures }; use constant SOCKET_INACTIVITY_TIMEOUT => 180; sub main { my $c = shift; my $ip = $c->tx->remote_address; my $key = $c->req->headers->header('Sec-WebSocket-Key'); $c->inactivity_timeout(SOCKET_INACTIVITY_TIMEOUT); if ($c->req->headers->header('Sec-WebSocket-Extensions') =~ /permessage-deflate/) { $c->tx->compressed(1); $c->res->headers->add('Sec-WebSocket-Extensions' => 'permessage-deflate'); } $c->redis->pubsub->listen( notify => sub($pubsub, $payload) { $c->send($payload); } ); $c->on(json => sub( $c, $message ) { if ( $message->{event} eq 'KEEPALIVE' ) { my $user; if ($message->{payload} =~ /^\d+$/) { $user = $c->pg->db->select('users', [qw(id username secret jitsi_allowed roles banned_until)], { id => $message->{payload}} )->hash; } if ( $user ) { my $sig = hmac_sha1_hex($message->{payload}, $user->{secret} ); if ( $sig ne $message->{sig} ) { $c->app->log->warn( "Invalid signature for " . $user->{username} ); $user = undef; } } eval { my $tx = $c->pg->db->begin; $c->pg->db->delete('sockets', [ {id => $key}, $user ? {user_id => $user->{id}}:{} ]); $c->pg->db->insert('sockets', { id => $key, ip => $ip, keepalive => \'now()', user_id => $user ? $user->{id} : undef, }); $tx->commit; }; my $all = $c->pg->db->query( 'select count(*) from sockets_view' )->hash->{count}; my $members = $c->pg->db->query( 'select count(*) from sockets_view where is_member' )->hash->{count}; my $group_size = $c->_member_group_size($members); $c->send({json => { event => 'online_users_updated', payload => { all => $all, members => $members, group_size_full => $group_size->{full}, group_size_half => $group_size->{half}, }}}); if ( $user ) { my $jitsi = $user->{jitsi_allowed} || $user->{roles} =~ /chairman|jitsi/; $c->send({json => { event => 'user_status', payload => { jitsi_allowed => $jitsi ? \1:\0, is_banned => $user->{banned_until} ? \1:\0, }}}); } } }); $c->on(finish => sub ($c, $code, $reason = undef) { $c->redis->pubsub->unlisten('notify'); $c->app->log->debug("WebSocket closed with status $code"); }); } sub _member_group_size ($c, $total = 0){ my $group = 2 * sqrt($total); my $min = $total / 100; my $max = $total / 5; $group = $min if $group < $min; $group = $max if $group > $max; return { full => ceil( $group ), half => ceil( $group/2 ), }; } 1;