diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php index 8273720b..f3e374c0 100644 --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -1,388 +1,390 @@ first(); // This isn't a room, bye bye if (!$room) { return $this->errorResponse(404, \trans('meet.room-not-found')); } $user = Auth::guard()->user(); // Only the room owner can do it if (!$user || $user->id != $room->user_id) { return $this->errorResponse(403); } if (!$room->requestAccept($reqid)) { return $this->errorResponse(500, \trans('meet.session-request-accept-error')); } return response()->json(['status' => 'success']); } /** * Denying the room join request. * * @param string $id Room identifier (name) * @param string $reqid Request identifier * * @return \Illuminate\Http\JsonResponse */ public function denyJoinRequest($id, $reqid) { $room = Room::where('name', $id)->first(); // This isn't a room, bye bye if (!$room) { return $this->errorResponse(404, \trans('meet.room-not-found')); } $user = Auth::guard()->user(); // Only the room owner can do it if (!$user || $user->id != $room->user_id) { return $this->errorResponse(403); } if (!$room->requestDeny($reqid)) { return $this->errorResponse(500, \trans('meet.session-request-deny-error')); } return response()->json(['status' => 'success']); } /** * Close the room session. * * @param string $id Room identifier (name) * * @return \Illuminate\Http\JsonResponse */ public function closeRoom($id) { $room = Room::where('name', $id)->first(); // This isn't a room, bye bye if (!$room) { return $this->errorResponse(404, \trans('meet.room-not-found')); } $user = Auth::guard()->user(); // Only the room owner can do it if (!$user || $user->id != $room->user_id) { return $this->errorResponse(403); } if (!$room->deleteSession()) { return $this->errorResponse(500, \trans('meet.session-close-error')); } return response()->json([ 'status' => 'success', 'message' => __('meet.session-close-success'), ]); } /** * Accepting the room join request. * * @param string $id Room identifier (name) * @param string $conn Connection identifier * * @return \Illuminate\Http\JsonResponse */ public function dismissConnection($id, $conn) { $room = Room::where('name', $id)->first(); // This isn't a room, bye bye if (!$room) { return $this->errorResponse(404, \trans('meet.room-not-found')); } $user = Auth::guard()->user(); // Only the room owner can do it if (!$user || $user->id != $room->user_id) { return $this->errorResponse(403); } if (!$room->closeOVConnection($conn)) { return $this->errorResponse(500, \trans('meet.session-dismiss-connection-error')); } return response()->json(['status' => 'success']); } /** * Listing of rooms that belong to the current user. * * @return \Illuminate\Http\JsonResponse */ public function index() { $user = Auth::guard()->user(); $rooms = Room::where('user_id', $user->id)->orderBy('name')->get(); if (count($rooms) == 0) { // Create a room for the user (with a random and unique name) while (true) { $name = strtolower(\App\Utils::randStr(3, 3, '-')); if (!Room::where('name', $name)->count()) { break; } } $room = Room::create([ 'name' => $name, 'user_id' => $user->id ]); $rooms = collect([$room]); } $result = [ 'list' => $rooms, 'count' => count($rooms), ]; return response()->json($result); } /** * Join the room session. Each room has one owner, and the room isn't open until the owner * joins (and effectively creates the session). * * @param string $id Room identifier (name) * * @return \Illuminate\Http\JsonResponse */ public function joinRoom($id) { $room = Room::where('name', $id)->first(); // Room does not exist, or the owner is deleted if (!$room || !$room->owner) { return $this->errorResponse(404, \trans('meet.room-not-found')); } // Check if there's still a valid beta entitlement for the room owner $sku = \App\Sku::where('title', 'meet')->first(); if ($sku && !$room->owner->entitlements()->where('sku_id', $sku->id)->first()) { return $this->errorResponse(404, \trans('meet.room-not-found')); } $user = Auth::guard()->user(); $isOwner = $user && $user->id == $room->user_id; // There's no existing session if (!$room->hasSession()) { // Participants can't join the room until the session is created by the owner if (!$isOwner) { - return $this->errorResponse(423, \trans('meet.session-not-found')); + return $this->errorResponse(422, \trans('meet.session-not-found'), ['code' => 323]); } // The room owner can create the session on request if (empty(request()->input('init'))) { - return $this->errorResponse(424, \trans('meet.session-not-found')); + return $this->errorResponse(422, \trans('meet.session-not-found'), ['code' => 324]); } $session = $room->createSession(); if (empty($session)) { return $this->errorResponse(500, \trans('meet.session-create-error')); } } $password = (string) $room->getSetting('password'); $config = [ 'locked' => $room->getSetting('locked') === 'true', 'password' => $isOwner ? $password : '', 'requires_password' => !$isOwner && strlen($password), ]; + $response = ['config' => $config]; + // Validate room password if (!$isOwner && strlen($password)) { $request_password = request()->input('password'); if ($request_password !== $password) { - return $this->errorResponse(425, \trans('meet.session-password-error'), ['config' => $config]); + return $this->errorResponse(422, \trans('meet.session-password-error'), $response + ['code' => 325]); } } // Handle locked room if (!$isOwner && $config['locked']) { $nickname = request()->input('nickname'); $picture = request()->input('picture'); $requestId = request()->input('requestId'); $request = $requestId ? $room->requestGet($requestId) : null; $error = \trans('meet.session-room-locked-error'); // Request already has been processed (not accepted yet, but it could be denied) if (empty($request['status']) || $request['status'] != Room::REQUEST_ACCEPTED) { if (!$request) { if (empty($nickname) || empty($requestId) || !preg_match('/^[a-z0-9]{8,32}$/i', $requestId)) { - return $this->errorResponse(426, $error, ['config' => $config]); + return $this->errorResponse(422, $error, $response + ['code' => 326]); } if (empty($picture)) { $svg = file_get_contents(resource_path('images/user.svg')); $picture = 'data:image/svg+xml;base64,' . base64_encode($svg); } elseif (!preg_match('|^data:image/png;base64,[a-zA-Z0-9=+/]+$|', $picture)) { - return $this->errorResponse(426, $error, ['config' => $config]); + return $this->errorResponse(422, $error, $response + ['code' => 326]); } // TODO: Resize when big/make safe the user picture? $request = ['nickname' => $nickname, 'requestId' => $requestId, 'picture' => $picture]; if (!$room->requestSave($requestId, $request)) { // FIXME: should we use error code 500? - return $this->errorResponse(426, $error, ['config' => $config]); + return $this->errorResponse(422, $error, $response + ['code' => 326]); } // Send the request (signal) to the owner $result = $room->signal('joinRequest', $request, Room::ROLE_MODERATOR); } - return $this->errorResponse(427, $error, ['config' => $config]); + return $this->errorResponse(422, $error, $response + ['code' => 327]); } } // Create session token for the current user/connection $response = $room->getSessionToken($isOwner ? Room::ROLE_MODERATOR : Room::ROLE_PUBLISHER); if (empty($response)) { return $this->errorResponse(500, \trans('meet.session-join-error')); } // Create session token for screen sharing connection if (!empty(request()->input('screenShare'))) { $add_token = $room->getSessionToken(Room::ROLE_PUBLISHER); $response['shareToken'] = $add_token['token']; } // Tell the UI who's the room owner $response['owner'] = $isOwner; // Append the room configuration $response['config'] = $config; return response()->json($response); } /** * Set the domain configuration. * * @param string $id Room identifier (name) * * @return \Illuminate\Http\JsonResponse|void */ public function setRoomConfig($id) { $room = Room::where('name', $id)->first(); // Room does not exist, or the owner is deleted if (!$room || !$room->owner) { return $this->errorResponse(404); } $user = Auth::guard()->user(); // Only room owner can configure the room if ($user->id != $room->user_id) { return $this->errorResponse(403); } $input = request()->input(); $errors = []; foreach ($input as $key => $value) { switch ($key) { case 'password': if ($value === null || $value === '') { $input[$key] = null; } else { // TODO: Do we have to validate the password in any way? } break; case 'locked': $input[$key] = $value ? 'true' : null; break; default: $errors[$key] = \trans('meet.room-unsupported-option-error'); } } if (!empty($errors)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } if (!empty($input)) { $room->setSettings($input); } return response()->json([ 'status' => 'success', 'message' => \trans('meet.room-setconfig-success'), ]); } /** * Webhook as triggered from OpenVidu server * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\Response The response */ public function webhook(Request $request) { \Log::debug($request->getContent()); switch ((string) $request->input('event')) { case 'sessionDestroyed': // When all participants left the room OpenVidu dispatches sessionDestroyed // event. We'll remove the session reference from the database. $sessionId = $request->input('sessionId'); $room = Room::where('session_id', $sessionId)->first(); if ($room) { $room->session_id = null; $room->save(); } break; } return response('Success', 200); } } diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue index a7daa277..77ba8621 100644 --- a/src/resources/vue/Meet/Room.vue +++ b/src/resources/vue/Meet/Room.vue @@ -1,515 +1,521 @@ diff --git a/src/resources/vue/Widgets/StatusMessage.vue b/src/resources/vue/Widgets/StatusMessage.vue index 71935346..80fd3120 100644 --- a/src/resources/vue/Widgets/StatusMessage.vue +++ b/src/resources/vue/Widgets/StatusMessage.vue @@ -1,49 +1,49 @@ diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php index 67f1f0c0..ab47aed6 100644 --- a/src/tests/Browser/Meet/RoomControlsTest.php +++ b/src/tests/Browser/Meet/RoomControlsTest.php @@ -1,342 +1,338 @@ clearBetaEntitlements(); } public function tearDown(): void { $this->clearBetaEntitlements(); parent::tearDown(); } /** * Test fullscreen buttons * * @group openvidu */ public function testFullscreen(): void { // TODO: This test does not work in headless mode $this->markTestIncomplete(); // Make sure there's no session yet $room = Room::where('name', 'john')->first(); if ($room->session_id) { $room->session_id = null; $room->save(); } $this->assignBetaEntitlement('john@kolab.org', 'meet'); $this->browse(function (Browser $browser) { // Join the room as an owner (authenticate) $browser->visit(new RoomPage('john')) ->click('@setup-button') ->assertMissing('@toolbar') ->assertMissing('@menu') ->assertMissing('@session') ->assertMissing('@chat') ->assertMissing('@setup-form') ->assertVisible('@login-form') ->submitLogon('john@kolab.org', 'simple123') ->waitFor('@setup-form') ->assertMissing('@login-form') ->waitUntilMissing('@setup-status-message.loading') ->click('@setup-button') ->waitFor('@session') // Test fullscreen for the whole room ->click('@menu button.link-fullscreen.closed') ->assertVisible('@toolbar') ->assertVisible('@session') ->assertMissing('nav') ->assertMissing('@menu button.link-fullscreen.closed') ->click('@menu button.link-fullscreen.open') ->assertVisible('nav') // Test fullscreen for the participant video ->click('@session button.link-fullscreen.closed') ->assertVisible('@session') ->assertMissing('@toolbar') ->assertMissing('nav') ->assertMissing('@session button.link-fullscreen.closed') ->click('@session button.link-fullscreen.open') ->assertVisible('nav') ->assertVisible('@toolbar'); }); } /** * Test nickname and muting audio/video * * @group openvidu */ public function testNicknameAndMuting(): void { // Make sure there's no session yet $room = Room::where('name', 'john')->first(); if ($room->session_id) { $room->session_id = null; $room->save(); } $this->assignBetaEntitlement('john@kolab.org', 'meet'); $this->browse(function (Browser $owner, Browser $guest) { // Join the room as an owner (authenticate) $owner->visit(new RoomPage('john')) ->click('@setup-button') ->submitLogon('john@kolab.org', 'simple123') ->waitFor('@setup-form') ->waitUntilMissing('@setup-status-message.loading') ->type('@setup-nickname-input', 'john') ->click('@setup-button') ->waitFor('@session'); // In another browser act as a guest $guest->visit(new RoomPage('john')) ->waitFor('@setup-form') ->waitUntilMissing('@setup-status-message.loading') ->assertMissing('@setup-status-message') ->assertSeeIn('@setup-button', "JOIN") // Join the room, disable cam/mic ->select('@setup-mic-select', '') ->select('@setup-cam-select', '') ->click('@setup-button') ->waitFor('@session'); // Assert current UI state $owner->assertToolbar([ 'audio' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'video' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'security' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, ]) ->whenAvailable('div.meet-video.publisher', function (Browser $browser) { $browser->assertVisible('video') ->assertAudioMuted('video', true) ->assertSeeIn('.nickname', 'john') - ->assertMissing('.nickname button') ->assertVisible('.controls button.link-fullscreen') ->assertMissing('.controls button.link-audio') ->assertMissing('.status .status-audio') ->assertMissing('.status .status-video'); }) ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) { $browser->assertMissing('video') - ->assertMissing('.nickname') + ->assertVisible('.nickname') ->assertVisible('.controls button.link-fullscreen') ->assertVisible('.controls button.link-audio') ->assertVisible('.status .status-audio') ->assertVisible('.status .status-video'); }) ->assertElementsCount('@session div.meet-video', 2); // Assert current UI state $guest->assertToolbar([ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, ]) ->whenAvailable('div.meet-video.publisher', function (Browser $browser) { $browser->assertVisible('video') //->assertAudioMuted('video', true) - ->assertVisible('.nickname button') - ->assertMissing('.nickname span') ->assertVisible('.controls button.link-fullscreen') ->assertMissing('.controls button.link-audio') ->assertVisible('.status .status-audio') ->assertVisible('.status .status-video'); }) ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) { $browser->assertVisible('video') ->assertSeeIn('.nickname', 'john') - ->assertMissing('.nickname button') ->assertVisible('.controls button.link-fullscreen') ->assertVisible('.controls button.link-audio') ->assertMissing('.status .status-audio') ->assertMissing('.status .status-video'); }) ->assertElementsCount('@session div.meet-video', 2); // Test nickname change propagation // Use script() because type() does not work with this contenteditable widget $guest->setNickname('div.meet-video.publisher', 'guest'); $owner->waitFor('div.meet-video:not(.publisher) .nickname') ->assertSeeIn('div.meet-video:not(.publisher) .nickname', 'guest'); // Test muting audio $owner->click('@menu button.link-audio') ->assertToolbarButtonState('audio', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED) ->assertVisible('div.meet-video.publisher .status .status-audio'); // FIXME: It looks that we can't just check the