diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\API\V4; use App\Http\Controllers\Controller; +use App\OpenVidu\Connection; use App\OpenVidu\Room; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -115,22 +116,22 @@ */ public function dismissConnection($id, $conn) { - $room = Room::where('name', $id)->first(); + $connection = Connection::where('id', $conn)->first(); - // This isn't a room, bye bye - if (!$room) { - return $this->errorResponse(404, \trans('meet.room-not-found')); + // There's no such connection, bye bye + if (!$connection || $connection->room->name != $id) { + return $this->errorResponse(404, \trans('meet.connection-not-found')); } $user = Auth::guard()->user(); - // Only the room owner can do it - if (!$user || $user->id != $room->user_id) { + // Only the room owner can do it (for now) + if (!$user || $user->id != $connection->room->user_id) { return $this->errorResponse(403); } - if (!$room->closeOVConnection($conn)) { - return $this->errorResponse(500, \trans('meet.session-dismiss-connection-error')); + if (!$connection->dismiss()) { + return $this->errorResponse(500, \trans('meet.connection-dismiss-error')); } return response()->json(['status' => 'success']); @@ -281,28 +282,29 @@ if ($init) { // Choose the connection role $canPublish = !empty(request()->input('canPublish')); - $reqRole = $canPublish ? Room::ROLE_PUBLISHER : Room::ROLE_SUBSCRIBER; - $role = $isOwner ? Room::ROLE_MODERATOR : $reqRole; + $role = $canPublish ? Room::ROLE_PUBLISHER : Room::ROLE_SUBSCRIBER; + if ($isOwner) { + $role |= Room::ROLE_MODERATOR; + $role |= Room::ROLE_OWNER; + } // Create session token for the current user/connection - $response = $room->getSessionToken($role, ['canPublish' => $canPublish]); + $response = $room->getSessionToken($role); if (empty($response)) { return $this->errorResponse(500, \trans('meet.session-join-error')); } // Create session token for screen sharing connection - if ($role != Room::ROLE_SUBSCRIBER && !empty(request()->input('screenShare'))) { - $add_token = $room->getSessionToken(Room::ROLE_PUBLISHER, ['canPublish' => true]); + if (($role & Room::ROLE_PUBLISHER) && !empty(request()->input('screenShare'))) { + $add_token = $room->getSessionToken(Room::ROLE_SCREEN); $response['shareToken'] = $add_token['token']; } $response_code = 200; $response['role'] = $role; - $response['owner'] = $isOwner; $response['config'] = $config; - $response['canPublish'] = $canPublish; } else { $response_code = 422; $response['code'] = 322; @@ -393,6 +395,12 @@ $room->save(); } + // Remove all connections + // Note: We could remove connections one-by-one via the 'participantLeft' event + // but that could create many INSERTs when the session (with many participants) ends + // So, it is better to remove them all in a single INSERT. + Connection::where('session_id', $sessionId)->delete(); + break; } diff --git a/src/app/OpenVidu/Connection.php b/src/app/OpenVidu/Connection.php new file mode 100644 --- /dev/null +++ b/src/app/OpenVidu/Connection.php @@ -0,0 +1,57 @@ + 'array', + ]; + + /** + * Dismiss (close) the connection. + * + * @return bool True on success, False on failure + */ + public function dismiss() + { + if ($this->room->closeOVConnection($this->id)) { + $this->delete(); + + return true; + } + + return false; + } + + /** + * The room to which this connection belongs. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function room() + { + return $this->belongsTo(Room::class, 'room_id', 'id'); + } +} diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -18,13 +18,19 @@ { use SettingsTrait; - public const ROLE_MODERATOR = 'MODERATOR'; - public const ROLE_PUBLISHER = 'PUBLISHER'; - public const ROLE_SUBSCRIBER = 'SUBSCRIBER'; + public const ROLE_SUBSCRIBER = 1 << 0; + public const ROLE_PUBLISHER = 1 << 1; + public const ROLE_MODERATOR = 1 << 2; + public const ROLE_SCREEN = 1 << 3; + public const ROLE_OWNER = 1 << 4; public const REQUEST_ACCEPTED = 'accepted'; public const REQUEST_DENIED = 'denied'; + private const OV_ROLE_MODERATOR = 'MODERATOR'; + private const OV_ROLE_PUBLISHER = 'PUBLISHER'; + private const OV_ROLE_SUBSCRIBER = 'SUBSCRIBER'; + protected $fillable = [ 'user_id', 'name' @@ -169,14 +175,12 @@ /** * Create a OpenVidu session (connection) token * - * @param string $role User role - * @param array $data User data to attach to the connection. - * It will be available client-side for everybody. + * @param int $role User role (see self::ROLE_* constants) * * @return array|null Token data on success, NULL otherwise * @throws \Exception if session does not exist */ - public function getSessionToken($role = self::ROLE_PUBLISHER, $data = []): ?array + public function getSessionToken($role = self::ROLE_SUBSCRIBER): ?array { if (!$this->session_id) { throw new \Exception("The room session does not exist"); @@ -186,16 +190,12 @@ // to make it visible for everyone in a room. So, for example we can // handle/style subscribers/publishers/moderators differently on the // client-side. Is this a security issue? - if (!empty($data)) { - $data += ['role' => $role]; - } else { - $data = ['role' => $role]; - } + $data = ['role' => $role]; $url = 'sessions/' . $this->session_id . '/connection'; $post = [ 'json' => [ - 'role' => $role, + 'role' => self::OV_ROLE_PUBLISHER, 'data' => json_encode($data) ] ]; @@ -205,11 +205,26 @@ if ($response->getStatusCode() == 200) { $json = json_decode($response->getBody(), true); + // Extract the 'token' part of the token, it will be used to authenticate the connection. + // It will be needed in next iterations e.g. to authenticate moderators that aren't + // Kolab4 users (or are just not logged in to Kolab4). + // FIXME: we could as well generate our own token for auth purposes + parse_str(parse_url($json['token'], PHP_URL_QUERY), $url); + + // Create the connection reference in our database + $conn = new Connection(); + $conn->id = $json['id']; + $conn->session_id = $this->session_id; + $conn->room_id = $this->id; + $conn->role = $role; + $conn->metadata = ['token' => $url['token']]; + $conn->save(); + return [ 'session' => $this->session_id, 'token' => $json['token'], - 'role' => $json['role'], 'connectionId' => $json['id'], + 'role' => $role, ]; } @@ -324,10 +339,10 @@ /** * Send a OpenVidu signal to the session participants (connections) * - * @param string $name Signal name (type) - * @param array $data Signal data array - * @param array|string $target List of target connections, Null for all connections. - * It can be also a participant role. + * @param string $name Signal name (type) + * @param array $data Signal data array + * @param null|int|string[] $target List of target connections, Null for all connections. + * It can be also a participant role. * * @return bool True on success, False on failure * @throws \Exception if session does not exist @@ -345,26 +360,11 @@ ]; // Get connection IDs by participant role - if (is_string($target)) { - // TODO: We should probably store this in our database/redis. I foresee a use-case - // for such a connections store on our side, e.g. keeping participant - // metadata, e.g. selected language, extra roles like a "language interpreter", etc. - - $response = $this->client()->request('GET', 'sessions/' . $this->session_id); - - if ($response->getStatusCode() !== 200) { - return false; - } - - $json = json_decode($response->getBody(), true); - $connections = []; - - foreach ($json['connections']['content'] as $connection) { - if ($connection['role'] === $target) { - $connections[] = $connection['id']; - break; - } - } + if (is_int($target)) { + $connections = Connection::where('session_id', $this->session_id) + ->whereRaw("(role & $target)") + ->pluck('id') + ->all(); if (empty($connections)) { return false; diff --git a/src/database/migrations/2021_01_13_120000_create_openvidu_connections_table.php b/src/database/migrations/2021_01_13_120000_create_openvidu_connections_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2021_01_13_120000_create_openvidu_connections_table.php @@ -0,0 +1,46 @@ +string('id', 24); + $table->string('session_id', 24); + $table->bigInteger('room_id')->unsigned(); + $table->smallInteger('role')->default(0); + $table->text('metadata')->nullable(); // should be json, but mariadb + $table->timestamps(); + + $table->primary('id'); + $table->index('session_id'); + $table->foreign('room_id')->references('id')->on('openvidu_rooms')->onDelete('cascade'); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('openvidu_connections'); + } +} diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js --- a/src/resources/js/meet/app.js +++ b/src/resources/js/meet/app.js @@ -2,6 +2,13 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { OpenVidu } from 'openvidu-browser' +class Roles { + static get SUBSCRIBER() { return 1 << 0; } + static get PUBLISHER() { return 1 << 1; } + static get MODERATOR() { return 1 << 2; } + static get SCREEN() { return 1 << 3; } + static get OWNER() { return 1 << 4; } +} function Meet(container) { @@ -14,7 +21,6 @@ let audioSource = '' // Currently selected microphone let videoSource = '' // Currently selected camera let sessionData // Room session metadata - let role // Current user role let screenOV // OpenVidu object to initialize a screen sharing session let screenSession // Session object where the user will connect for screen sharing @@ -67,12 +73,11 @@ this.switchVideo = switchVideo this.updateSession = updateSession - /** * Join the room session * - * @param data Session metadata and event handlers (session, token, shareToken, nickname, - * canPublish, chatElement, menuElement, onDestroy, onJoinRequest) + * @param data Session metadata and event handlers (session, token, shareToken, nickname, role, + * chatElement, menuElement, onDestroy, onJoinRequest) */ function joinRoom(data) { resize(); @@ -97,7 +102,6 @@ session.on('connectionCreated', event => { // Ignore the current user connection if (event.connection.role) { - role = event.connection.role return } @@ -182,7 +186,7 @@ session.connect(data.token, data.params) .then(() => { let wrapper - let params = { self: true, canPublish: data.canPublish, audioActive, videoActive } + let params = { self: true, role: data.role, audioActive, videoActive } params = Object.assign({}, data.params, params) publisher.on('videoElementCreated', event => { @@ -196,7 +200,7 @@ wrapper = participantCreate(params) - if (data.canPublish) { + if (data.role & Roles.PUBLISHER) { publisher.createVideoElement(wrapper, 'PREPEND') session.publish(publisher) } @@ -654,7 +658,7 @@ } /** - * Create a participant element in the matrix. Depending on the `canPublish` + * Create a participant element in the matrix. Depending on the connection role * parameter it will be a video element wrapper inside the matrix or a simple * tag-like element on the subscribers list. * @@ -663,7 +667,7 @@ * @return The element */ function participantCreate(params) { - if (params.canPublish) { + if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) { return publisherCreate(params) } @@ -760,7 +764,7 @@ $element.addClass('self') } - if (role == 'MODERATOR') { + if (sessionData.role & Roles.MODERATOR) { $element.addClass('moderated') } } @@ -827,7 +831,7 @@ return false } }) - } else if (role == 'MODERATOR') { + } else if (sessionData.role & Roles.MODERATOR) { nickname.attr({title: 'Options', 'data-toggle': 'dropdown'}) .dropdown({boundary: container}) @@ -1071,4 +1075,4 @@ } } -export default Meet +export { Meet, Roles } diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php --- a/src/resources/lang/en/meet.php +++ b/src/resources/lang/en/meet.php @@ -13,6 +13,8 @@ | */ + 'connection-not-found' => 'The connection does not exist.', + 'connection-dismiss-error' => 'Failed to dismiss the connection.', 'room-not-found' => 'The room does not exist.', 'room-setconfig-success' => 'Room configuration updated successfully.', 'room-unsupported-option-error' => 'Invalid room configuration option.', @@ -21,7 +23,6 @@ 'session-join-error' => 'Failed to join the session.', 'session-close-error' => 'Failed to close the session.', 'session-close-success' => 'The session has been closed successfully.', - 'session-dismiss-connection-error' => 'Failed to dismiss the connection.', 'session-password-error' => 'Failed to join the session. Invalid password.', 'session-request-accept-error' => 'Failed to accept the join request.', 'session-request-deny-error' => 'Failed to deny the join request.', diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue --- a/src/resources/vue/Meet/Room.vue +++ b/src/resources/vue/Meet/Room.vue @@ -20,7 +20,7 @@ -