diff --git a/js/signaling.js b/js/signaling.js index 8788d81adcf..868e98df360 100644 --- a/js/signaling.js +++ b/js/signaling.js @@ -137,6 +137,122 @@ return defer; }; + SignalingBase.prototype.joinRoom = function(token, password) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', + type: 'POST', + beforeSend: function (request) { + request.setRequestHeader('Accept', 'application/json'); + }, + data: { + password: password + }, + success: function (result) { + console.log("Joined", result); + this.currentRoomToken = token; + this._joinRoomSuccess(token, result.ocs.data.sessionId); + }.bind(this), + error: function (result) { + if (result.status === 404 || result.status === 503) { + // Room not found or maintenance mode + OC.redirect(OC.generateUrl('apps/spreed')); + } + + if (result.status === 403) { + // This should not happen anymore since we ask for the password before + // even trying to join the call, but let's keep it for now. + OC.dialogs.prompt( + t('spreed', 'Please enter the password for this call'), + t('spreed','Password required'), + function (result, password) { + if (result && password !== '') { + this.joinRoom(token, password); + } + }.bind(this), + true, + t('spreed','Password'), + true + ).then(function() { + var $dialog = $('.oc-dialog:visible'); + $dialog.find('.ui-icon').remove(); + + var $buttons = $dialog.find('button'); + $buttons.eq(0).text(t('core', 'Cancel')); + $buttons.eq(1).text(t('core', 'Submit')); + }); + } + }.bind(this) + }); + }; + + SignalingBase.prototype._leaveRoomSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.leaveRoom = function(token) { + this.leaveCurrentCall(); + this._doLeaveRoom(token); + + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', + method: 'DELETE', + async: false, + success: function () { + this._leaveRoomSuccess(token); + // We left the current room. + if (token === this.currentRoomToken) { + this.currentRoomToken = null; + } + }.bind(this) + }); + }; + + SignalingBase.prototype._joinCallSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.joinCall = function(token, callback) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, + type: 'POST', + beforeSend: function (request) { + request.setRequestHeader('Accept', 'application/json'); + }, + success: function () { + this.currentCallToken = token; + this._joinCallSuccess(token); + // We send an empty call description to simplewebrtc since + // usersChanged (webrtc.js) will create/remove peer connections + // with call participants + var callDescription = {'clients': {}}; + callback('', callDescription); + }.bind(this), + error: function () { + // Room not found or maintenance mode + OC.redirect(OC.generateUrl('apps/spreed')); + }.bind(this) + }); + }; + + SignalingBase.prototype._leaveCallSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.leaveCall = function(token) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, + method: 'DELETE', + async: false, + success: function () { + this._leaveCallSuccess(token); + // We left the current call. + if (token === this.currentCallToken) { + this.currentCallToken = null; + } + }.bind(this) + }); + }; + // Connection to the internal signaling server provided by the app. function InternalSignaling(/*settings*/) { SignalingBase.prototype.constructor.apply(this, arguments); @@ -221,101 +337,17 @@ return defer; }; - InternalSignaling.prototype.joinRoom = function(token, password) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', - type: 'POST', - beforeSend: function (request) { - request.setRequestHeader('Accept', 'application/json'); - }, - data: { - password: password - }, - success: function (result) { - console.log("Joined", result); - this.sessionId = result.ocs.data.sessionId; - this.currentRoomToken = token; - this._startPingCall(); - this._startPullingMessages(); - }.bind(this), - error: function (result) { - if (result.status === 404 || result.status === 503) { - // Room not found or maintenance mode - OC.redirect(OC.generateUrl('apps/spreed')); - } - - if (result.status === 403) { - // This should not happen anymore since we ask for the password before - // even trying to join the call, but let's keep it for now. - OC.dialogs.prompt( - t('spreed', 'Please enter the password for this call'), - t('spreed','Password required'), - function (result, password) { - if (result && password !== '') { - this.joinRoom(token, password); - } - }.bind(this), - true, - t('spreed','Password'), - true - ).then(function() { - var $dialog = $('.oc-dialog:visible'); - $dialog.find('.ui-icon').remove(); - - var $buttons = $dialog.find('button'); - $buttons.eq(0).text(t('core', 'Cancel')); - $buttons.eq(1).text(t('core', 'Submit')); - }); - } - }.bind(this) - }); + InternalSignaling.prototype._joinRoomSuccess = function(token, sessionId) { + this.sessionId = sessionId; + this._startPingCall(); + this._startPullingMessages(); }; - InternalSignaling.prototype.leaveRoom = function(token) { - if (this.currentCallToken) { - this.leaveCall(); - } - + InternalSignaling.prototype._doLeaveRoom = function(token) { if (token === this.currentRoomToken) { this._stopPingCall(); this._closeEventSource(); } - - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', - method: 'DELETE', - async: false - }); - }; - - InternalSignaling.prototype.joinCall = function(token, callback) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, - type: 'POST', - beforeSend: function (request) { - request.setRequestHeader('Accept', 'application/json'); - }, - success: function () { - this.currentCallToken = token; - // We send an empty call description to simplewebrtc since - // usersChanged (webrtc.js) will create/remove peer connections - // with call participants - var callDescription = {'clients': {}}; - callback('', callDescription); - }.bind(this), - error: function () { - // Room not found or maintenance mode - OC.redirect(OC.generateUrl('apps/spreed')); - }.bind(this) - }); - }; - - InternalSignaling.prototype.leaveCall = function(token) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, - method: 'DELETE', - async: false - }); }; InternalSignaling.prototype.sendCallMessage = function(data) { @@ -517,6 +549,7 @@ this.maxReconnectIntervalMs = 16000; this.reconnectIntervalMs = this.initialReconnectIntervalMs; this.joinedUsers = {}; + this.rooms = []; this.connect(); } @@ -588,10 +621,13 @@ } break; case "room": - if (this.currentCallToken && data.room.roomid !== this.currentCallToken) { - this._trigger('roomChanged', [this.currentCallToken, data.room.roomid]); + if (this.currentRoomToken && data.room.roomid !== this.currentRoomToken) { + this._trigger('roomChanged', [this.currentRoomToken, data.room.roomid]); this.joinedUsers = {}; - this.currentCallToken = null; + this.currentRoomToken = null; + } else { + // TODO(fancycode): Only fetch properties of room that was modified. + this.internalSyncRooms(); } break; case "event": @@ -665,13 +701,13 @@ }; } else { var user = OC.getCurrentUser(); - var url = OC.generateUrl("/ocs/v2.php/apps/spreed/api/v1/signaling/backend"); + var url = OC.linkToOCS('apps/spreed/api/v1/signaling', 2) + 'backend'; msg = { "type": "hello", "hello": { "version": "1.0", "auth": { - "url": OC.getProtocol() + "://" + OC.getHost() + url, + "url": url, "params": { "userid": user.uid, "ticket": this.settings.ticket @@ -726,26 +762,44 @@ // so perform resync once. this.internalSyncRooms(); } - if (!resumedSession && this.currentCallToken) { - this.joinCall(this.currentCallToken); + if (!resumedSession && this.currentRoomToken) { + this.joinRoom(this.currentRoomToken); } }; - StandaloneSignaling.prototype.joinCall = function(token, callback) { - console.log("Join call", token); + StandaloneSignaling.prototype.setRoom = function(/* room */) { + SignalingBase.prototype.setRoom.apply(this, arguments); + return this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype._joinRoomSuccess = function(token, nextcloudSessionId) { + console.log("Join room", token); this.doSend({ "type": "room", "room": { - "roomid": token + "roomid": token, + // Pass the Nextcloud session id to the signaling server. The + // session id will be passed through to Nextcloud to check if + // the (Nextcloud) user is allowed to join the room. + "sessionid": nextcloudSessionId, } }, function(data) { - this.joinResponseReceived(data, token, callback); + this.joinResponseReceived(data, token); }.bind(this)); }; - StandaloneSignaling.prototype.joinResponseReceived = function(data, token, callback) { + StandaloneSignaling.prototype._joinCallSuccess = function(/* token */) { + // Update room list to fetch modified properties. + this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype._leaveCallSuccess = function(/* token */) { + // Update room list to fetch modified properties. + this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype.joinResponseReceived = function(data, token) { console.log("Joined", data, token); - this.currentCallToken = token; if (this.roomCollection) { // The list of rooms is not fetched from the server. Update ping // of joined room so it gets sorted to the top. @@ -756,16 +810,10 @@ }); this.roomCollection.sort(); } - if (callback) { - var roomDescription = { - "clients": {} - }; - callback('', roomDescription); - } }; - StandaloneSignaling.prototype.leaveCall = function(token) { - console.log("Leave call", token); + StandaloneSignaling.prototype._doLeaveRoom = function(token) { + console.log("Leave room", token); this.doSend({ "type": "room", "room": { @@ -779,7 +827,6 @@ this._trigger("usersLeft", [leftUsers]); } this.joinedUsers = {}; - this.currentCallToken = null; }.bind(this)); }; @@ -791,6 +838,9 @@ case "roomlist": this.processRoomListEvent(data); break; + case "participants": + this.processRoomParticipantsEvent(data); + break; default: console.log("Unsupported event target", data); break; @@ -845,15 +895,32 @@ }; StandaloneSignaling.prototype.syncRooms = function() { + if (this.pending_sync) { + // A sync request is already in progress, don't start another one. + return this.pending_sync; + } + // Never manually sync rooms, will be done based on notifications // from the signaling server. var defer = $.Deferred(); - defer.resolve([]); + defer.resolve(this.rooms); return defer; }; StandaloneSignaling.prototype.internalSyncRooms = function() { - return SignalingBase.prototype.syncRooms.apply(this, arguments); + if (this.pending_sync) { + // A sync request is already in progress, don't start another one. + return this.pending_sync; + } + + var defer = $.Deferred(); + this.pending_sync = SignalingBase.prototype.syncRooms.apply(this, arguments); + this.pending_sync.then(function(rooms) { + this.pending_sync = null; + this.rooms = rooms; + defer.resolve(rooms); + }.bind(this)); + return defer; }; StandaloneSignaling.prototype.processRoomListEvent = function(data) { @@ -861,6 +928,18 @@ this.internalSyncRooms(); }; + StandaloneSignaling.prototype.processRoomParticipantsEvent = function(data) { + switch (data.event.type) { + case "update": + this._trigger("usersChanged", [data.event.update.users]); + this.internalSyncRooms(); + break; + default: + console.log("Unknown room participant event", data); + break; + } + }; + OCA.SpreedMe.createSignalingConnection = function() { var settings = $("#app #signaling-settings").text(); if (settings) { diff --git a/js/webrtc.js b/js/webrtc.js index 870f657a22f..55ba34f604d 100644 --- a/js/webrtc.js +++ b/js/webrtc.js @@ -12,6 +12,7 @@ var spreedPeerConnectionTable = []; OCA.SpreedMe = OCA.SpreedMe || {}; var previousUsersInRoom = []; + var usersInCallMapping = {}; function updateParticipantsUI(currentUsersNo) { 'use strict'; @@ -110,6 +111,49 @@ var spreedPeerConnectionTable = []; updateParticipantsUI(previousUsersInRoom.length + 1); } + function usersInCallChanged(users) { + // The passed list are the users that are currently in the room, + // i.e. that are in the call and should call each other. + var currentSessionId = webrtc.connection.getSessionid(); + var currentUsersInRoom = []; + var userMapping = {}; + var selfInCall = false; + var sessionId; + for (sessionId in users) { + if (!users.hasOwnProperty(sessionId)) { + continue; + } + var user = users[sessionId]; + if (!user.inCall) { + continue; + } + + if (sessionId === currentSessionId) { + selfInCall = true; + continue; + } + + currentUsersInRoom.push(sessionId); + userMapping[sessionId] = user; + } + + if (!selfInCall) { + // Own session is no longer in the call, disconnect from all others. + usersChanged([], previousUsersInRoom); + return; + } + + var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom); + var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom); + var newUsers = []; + newSessionIds.forEach(function(sessionId) { + newUsers.push(userMapping[sessionId]); + }); + if (newUsers.length || disconnectedSessionIds.length) { + usersChanged(newUsers, disconnectedSessionIds); + } + } + function initWebRTC() { 'use strict'; Array.prototype.diff = function(a) { @@ -119,47 +163,26 @@ var spreedPeerConnectionTable = []; }; var signaling = OCA.SpreedMe.createSignalingConnection(); - signaling.on('usersJoined', function(users) { - usersChanged(users, []); - }); signaling.on('usersLeft', function(users) { + users.forEach(function(user) { + delete usersInCallMapping[user]; + }); usersChanged([], users); }); - signaling.on('usersInRoom', function(users) { - // The passed list are the users that are currently in the room, - // i.e. that are in the call and should call each other. - var currentSessionId = webrtc.connection.getSessionid(); - var currentUsersInRoom = []; - var userMapping = {}; - var selfInCall = false; + signaling.on('usersChanged', function(users) { users.forEach(function(user) { - if (!user['inCall']) { - return; - } - - var sessionId = user['sessionId'] || user.sessionid; - if (sessionId === currentSessionId) { - selfInCall = true; - return; - } - - currentUsersInRoom.push(sessionId); - userMapping[sessionId] = user; + var sessionId = user.sessionId || user.sessionid; + usersInCallMapping[sessionId] = user; }); - - if (!selfInCall) { - return; - } - - var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom); - var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom); - var newUsers = []; - newSessionIds.forEach(function(sessionId) { - newUsers.push(userMapping[sessionId]); + usersInCallChanged(usersInCallMapping); + }); + signaling.on('usersInRoom', function(users) { + usersInCallMapping = {}; + users.forEach(function(user) { + var sessionId = user.sessionId || user.sessionid; + usersInCallMapping[sessionId] = user; }); - if (newUsers.length || disconnectedSessionIds.length) { - usersChanged(newUsers, disconnectedSessionIds); - } + usersInCallChanged(usersInCallMapping); }); var nick = OC.getCurrentUser()['displayName']; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index cefddbf31f7..f37b3e1214c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -92,10 +92,14 @@ protected function registerInternalSignalingHooks(EventDispatcherInterface $disp $dispatcher->addListener(Room::class . '::postSessionLeaveCall', $listener); } + protected function getBackendNotifier() { + return $this->getContainer()->query(BackendNotifier::class); + } + protected function registerSignalingBackendHooks(EventDispatcherInterface $dispatcher) { $dispatcher->addListener(Room::class . '::postAddUsers', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $participants= $event->getArgument('users'); @@ -103,14 +107,14 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postSetName', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $notifier->roomModified($room); }); $dispatcher->addListener(Room::class . '::postSetParticipantType', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); // The type of a participant has changed, notify all participants @@ -119,7 +123,7 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postDeleteRoom', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $participants = $event->getArgument('participants'); @@ -127,12 +131,28 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postRemoveUser', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $user = $event->getArgument('user'); $notifier->roomsDisinvited($room, [$user->getUID()]); }); + $dispatcher->addListener(Room::class . '::postSessionJoinCall', function(GenericEvent $event) { + /** @var BackendNotifier $notifier */ + $notifier = $this->getBackendNotifier(); + + $room = $event->getSubject(); + $sessionId = $event->getArgument('sessionId'); + $notifier->roomInCallChanged($room, true, [$sessionId]); + }); + $dispatcher->addListener(Room::class . '::postSessionLeaveCall', function(GenericEvent $event) { + /** @var BackendNotifier $notifier */ + $notifier = $this->getBackendNotifier(); + + $room = $event->getSubject(); + $sessionId = $event->getArgument('sessionId'); + $notifier->roomInCallChanged($room, false, [$sessionId]); + }); } protected function registerCallActivityHooks(EventDispatcherInterface $dispatcher) { diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 8815a2b8a01..4c76b25f980 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -25,12 +25,13 @@ use OCA\Spreed\Config; use OCA\Spreed\Exceptions\RoomNotFoundException; +use OCA\Spreed\Exceptions\ParticipantNotFoundException; use OCA\Spreed\Manager; +use OCA\Spreed\Participant; use OCA\Spreed\Room; use OCA\Spreed\Signaling\Messages; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\OCSController; use OCP\IDBConnection; use OCP\IRequest; @@ -241,6 +242,10 @@ protected function getUsersInRoom(Room $room) { * @return bool */ private function validateBackendRequest($data) { + if (!isset($_SERVER['HTTP_SPREED_SIGNALING_RANDOM']) || + !isset($_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'])) { + return false; + } $random = $_SERVER['HTTP_SPREED_SIGNALING_RANDOM']; if (empty($random) || strlen($random) < 32) { return false; @@ -253,6 +258,16 @@ private function validateBackendRequest($data) { return hash_equals($hash, strtolower($checksum)); } + /** + * Return the body of the backend request. This can be overridden in + * tests. + * + * @return string + */ + protected function getInputStream() { + return file_get_contents('php://input'); + } + /** * Backend API to query information required for standalone signaling * servers. @@ -263,12 +278,12 @@ private function validateBackendRequest($data) { * @NoCSRFRequired * @PublicPage * - * @return JSONResponse + * @return DataResponse */ public function backend() { - $json = file_get_contents('php://input'); + $json = $this->getInputStream(); if (!$this->validateBackendRequest($json)) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'invalid_request', @@ -278,19 +293,22 @@ public function backend() { } $message = json_decode($json, true); - switch ($message['type']) { + switch (isset($message['type']) ? $message['type'] : "") { case 'auth': // Query authentication information about a user. return $this->backendAuth($message['auth']); case 'room': // Query information about a room. return $this->backendRoom($message['room']); + case 'ping': + // Ping sessions connected to a room. + return $this->backendPing($message['ping']); default: - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'unknown_type', - 'message' => 'The given type ' . print_r($message, true) . ' is not supported.', + 'message' => 'The given type ' . json_encode($message) . ' is not supported.', ], ]); } @@ -300,7 +318,7 @@ private function backendAuth($auth) { $params = $auth['params']; $userId = $params['userid']; if (!$this->config->validateSignalingTicket($userId, $params['ticket'])) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'invalid_ticket', @@ -312,7 +330,7 @@ private function backendAuth($auth) { if (!empty($userId)) { $user = $this->userManager->get($userId); if (!$user instanceof IUser) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_user', @@ -334,16 +352,18 @@ private function backendAuth($auth) { 'displayname' => $user->getDisplayName(), ]; } - return new JSONResponse($response); + return new DataResponse($response); } - private function backendRoom($room) { - $roomId = $room['roomid']; - $userId = $room['userid']; + private function backendRoom($roomRequest) { + $roomId = $roomRequest['roomid']; + $userId = $roomRequest['userid']; + $sessionId = $roomRequest['sessionid']; + try { - $room = $this->manager->getRoomForParticipantByToken($roomId, $userId); + $room = $this->manager->getRoomByToken($roomId); } catch (RoomNotFoundException $e) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_room', @@ -352,12 +372,36 @@ private function backendRoom($room) { ]); } + $participant = null; if (!empty($userId)) { - // Rooms get sorted by last ping time for users, so make sure to - // update when a user joins a room. - $room->ping($userId, '0', time()); + // User trying to join room. + try { + $participant = $room->getParticipant($userId); + } catch (ParticipantNotFoundException $e) { + // Ignore, will check for public rooms below. + } } + if (empty($participant)) { + // User was not invited to the room, check for access to public room. + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + // Return generic error to avoid leaking which rooms exist. + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); + } + } + + // Rooms get sorted by last ping time for users, so make sure to + // update when a user joins a room. + $room->ping($userId, $sessionId, time()); + $response = [ 'type' => 'room', 'room' => [ @@ -369,7 +413,39 @@ private function backendRoom($room) { ], ], ]; - return new JSONResponse($response); + return new DataResponse($response); + } + + private function backendPing($request) { + try { + $room = $this->manager->getRoomByToken($request['roomid']); + } catch (RoomNotFoundException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'No such room.', + ], + ]); + } + + $now = time(); + foreach ($request['entries'] as $entry) { + if (array_key_exists('userid', $entry)) { + $room->ping($entry['userid'], $entry['sessionid'], $now); + } else { + $room->ping('', $entry['sessionid'], $now); + } + } + + $response = [ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $room->getToken(), + ], + ]; + return new DataResponse($response); } } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 37fc0cb5cb3..f5111bfb833 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -24,6 +24,7 @@ namespace OCA\Spreed\Signaling; use OCA\Spreed\Config; +use OCA\Spreed\Participant; use OCA\Spreed\Room; use OCP\Http\Client\IClientService; use OCP\ILogger; @@ -55,6 +56,20 @@ public function __construct(Config $config, $this->secureRandom = $secureRandom; } + /** + * Perform actual network request to the signaling backend. + * This can be overridden in tests. + */ + protected function doRequest($url, $params) { + if (defined('PHPUNIT_RUN')) { + // Don't perform network requests when running tests. + return; + } + + $client = $this->clientService->newClient(); + $client->post($url, $params); + } + /** * Perform a request to the signaling backend. * @@ -77,7 +92,6 @@ private function backendRequest($url, $data) { } else if (strpos($url, 'ws://') === 0) { $url = 'http://' . substr($url, 5); } - $client = $this->clientService->newClient(); $body = json_encode($data); $headers = [ 'Content-Type' => 'application/json', @@ -92,10 +106,10 @@ private function backendRequest($url, $data) { 'headers' => $headers, 'body' => $body, ]; - if (!$signaling['verify']) { + if (empty($signaling['verify'])) { $params['verify'] = false; } - $client->post($url, $params); + $this->doRequest($url, $params); } /** @@ -198,4 +212,48 @@ public function roomDeleted($room, $participants) { ]); } + /** + * The "in-call" status of the given session ids has changed.. + * + * @param Room $room + * @param bool $inCall + * @param array $sessionids + * @throws \Exception + */ + public function roomInCallChanged($room, $inCall, $sessionIds) { + $this->logger->info('Room in-call status changed: ' . $room->getToken() . ' ' . $inCall . ' ' . print_r($sessionIds, true)); + $changed = []; + $users = []; + $participants = $room->getParticipants(); + foreach ($participants['users'] as $userId => $participant) { + if ($participant['inCall']) { + $users[] = $participant; + } + if (in_array($participant['sessionId'], $sessionIds)) { + $participant['userId'] = $userId; + $changed[] = $participant; + } + } + foreach ($participants['guests'] as $participant) { + if (!isset($participant['participantType'])) { + $participant['participantType'] = Participant::GUEST; + } + if ($participant['inCall']) { + $users[] = $participant; + } + if (in_array($participant['sessionId'], $sessionIds)) { + $changed[] = $participant; + } + } + + $this->backendRequest('/api/v1/room/' . $room->getToken(), [ + 'type' => 'incall', + 'incall' => [ + 'incall' => $inCall, + 'changed' => $changed, + 'users' => $users + ], + ]); + } + } diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php new file mode 100644 index 00000000000..d3bffa9da84 --- /dev/null +++ b/tests/php/Controller/SignalingControllerTest.php @@ -0,0 +1,619 @@ +. + * + */ + +namespace OCA\Spreed\Tests\php\Controller; + +use OCA\Spreed\Config; +use OCA\Spreed\Controller\SignalingController; +use OCA\Spreed\Exceptions\ParticipantNotFoundException; +use OCA\Spreed\Exceptions\RoomNotFoundException; +use OCA\Spreed\Manager; +use OCA\Spreed\Participant; +use OCA\Spreed\Room; +use OCA\Spreed\Signaling\Messages; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; + +class CustomInputSignalingController extends SignalingController { + + private $inputStream; + + public function setInputStream($data) { + $this->inputStream = $data; + } + + protected function getInputStream() { + return $this->inputStream; + } + +} + +/** + * @group DB + */ +class SignalingControllerTest extends \Test\TestCase { + + /** @var OCA\Spreed\Config */ + private $config; + + /** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */ + private $session; + + /** @var \OCA\Spreed\Manager|\PHPUnit_Framework_MockObject_MockObject */ + protected $manager; + + /** @var \OCP\IDBConnection|\PHPUnit_Framework_MockObject_MockObject */ + protected $dbConnection; + + /** @var \OCA\Spreed\Signaling\Messages|\PHPUnit_Framework_MockObject_MockObject */ + protected $messages; + + /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + /** @var string */ + private $userId; + + /** @var CustomInputSignalingController */ + private $controller; + + public function setUp() { + parent::setUp(); + + $this->userId = 'testUser'; + $secureRandom = \OC::$server->getSecureRandom(); + $timeFactory = $this->createMock(ITimeFactory::class); + $config = \OC::$server->getConfig(); + $config->setAppValue('spreed', 'signaling_servers', json_encode([ + 'secret' => 'MySecretValue', + ])); + $config->setAppValue('spreed', 'signaling_ticket_secret', 'the-app-ticket-secret'); + $config->setUserValue($this->userId, 'spreed', 'signaling_ticket_secret', 'the-user-ticket-secret'); + $this->config = new Config($config, $secureRandom, $timeFactory); + $this->session = $this->createMock(ISession::class); + $this->dbConnection = \OC::$server->getDatabaseConnection(); + $this->manager = $this->createMock(Manager::class); + $this->messages = $this->createMock(Messages::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->recreateSignalingController(); + } + + private function recreateSignalingController() { + $this->controller = new CustomInputSignalingController( + 'spreed', + $this->createMock(\OCP\IRequest::class), + $this->config, + $this->session, + $this->manager, + $this->dbConnection, + $this->messages, + $this->userManager, + $this->userId + ); + } + + private function validateBackendRandom($data, $random, $checksum) { + if (empty($random) || strlen($random) < 32) { + return false; + } + if (empty($checksum)) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->config->getSignalingSecret()); + return hash_equals($hash, strtolower($checksum)); + } + + private function calculateBackendChecksum($data, $random) { + if (empty($random) || strlen($random) < 32) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->config->getSignalingSecret()); + return $hash; + } + + public function testBackendChecksums() { + // Test checksum generation / validation with the example from the API documentation. + $data = '{"type":"auth","auth":{"version":"1.0","params":{"hello":"world"}}}'; + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = '3c4a69ff328299803ac2879614b707c807b4758cf19450755c60656cac46e3bc'; + $this->assertEquals($checksum, $this->calculateBackendChecksum($data, $random)); + $this->assertTrue($this->validateBackendRandom($data, $random, $checksum)); + } + + private function performBackendRequest($data) { + if (!is_string($data)) { + $data = json_encode($data); + } + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = $this->calculateBackendChecksum($data, $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $this->controller->setInputStream($data); + return $this->controller->backend(); + } + + public function testBackendChecksumValidation() { + $data = '{}'; + + // Random and checksum missing. + $this->controller->setInputStream($data); + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + + // Invalid checksum. + $this->controller->setInputStream($data); + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = $this->calculateBackendChecksum('{"foo": "bar"}', $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + + // Short random + $this->controller->setInputStream($data); + $random = '12345'; + $checksum = $this->calculateBackendChecksum($data, $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + } + + public function testBackendUnsupportedType() { + $result = $this->performBackendRequest([ + 'type' => 'unsupported-type', + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'unknown_type', + 'message' => 'The given type {"type":"unsupported-type"} is not supported.', + ], + ], $result->getData()); + } + + public function testBackendAuth() { + // Check validating of tickets. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => $this->userId, + 'ticket' => 'invalid-ticket', + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_ticket', + 'message' => 'The given ticket is not valid for this user.', + ], + ], $result->getData()); + + // Check validating ticket for passed user. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => 'invalid-userid', + 'ticket' => $this->config->getSignalingTicket($this->userId), + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_ticket', + 'message' => 'The given ticket is not valid for this user.', + ], + ], $result->getData()); + + // Check validating of existing users. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => 'unknown-userid', + 'ticket' => $this->config->getSignalingTicket('unknown-userid'), + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_user', + 'message' => 'The given user does not exist.', + ], + ], $result->getData()); + + // Check successfull authentication of users. + $testUser = $this->createMock(IUser::class); + $testUser->expects($this->once()) + ->method('getDisplayName') + ->willReturn('Test User'); + $testUser->expects($this->once()) + ->method('getUID') + ->willReturn($this->userId); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->userId) + ->willReturn($testUser); + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => $this->userId, + 'ticket' => $this->config->getSignalingTicket($this->userId), + ], + ], + ]); + $this->assertSame([ + 'type' => 'auth', + 'auth' => [ + 'version' => '1.0', + 'userid' => $this->userId, + 'user' => [ + 'displayname' => 'Test User', + ], + ], + ], $result->getData()); + + // Check successfull authentication of anonymous participants. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => '', + 'ticket' => $this->config->getSignalingTicket(''), + ], + ], + ]); + $this->assertSame([ + 'type' => 'auth', + 'auth' => [ + 'version' => '1.0', + ], + ], $result->getData()); + } + + public function testBackendRoomUnknown() { + $roomToken = 'the-room'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willThrowException(new RoomNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => '', + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ], $result->getData()); + } + + public function testBackendRoomInvited() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipant') + ->with($this->userId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::ONE_TO_ONE_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => '', + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::ONE_TO_ONE_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomAnonymousPublic() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->with($sessionId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::PUBLIC_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => '', + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::PUBLIC_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomInvitedPublic() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipant') + ->with($this->userId) + ->willThrowException(new ParticipantNotFoundException()); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->with($sessionId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::PUBLIC_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::PUBLIC_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomAnonymousOneToOne() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->willThrowException(new ParticipantNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => '', + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ], $result->getData()); + } + + public function testBackendPingUnknownRoom() { + $roomToken = 'the-room'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willThrowException(new RoomNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => $this->userId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'No such room.', + ], + ], $result->getData()); + } + + public function testBackendPingUser() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('ping') + ->with($this->userId, $sessionId); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => $this->userId, + 'sessionid' => $sessionId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + ], + ], $result->getData()); + } + + public function testBackendPingAnonymous() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('ping') + ->with('', $sessionId); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => '', + 'sessionid' => $sessionId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + ], + ], $result->getData()); + } + +} diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php new file mode 100644 index 00000000000..2c0d20e7d5a --- /dev/null +++ b/tests/php/Signaling/BackendNotifierTest.php @@ -0,0 +1,347 @@ +. + * + */ + +namespace OCA\Spreed\Tests\php\Signaling; + +use OCA\Spreed\AppInfo\Application; +use OCA\Spreed\Config; +use OCA\Spreed\Manager; +use OCA\Spreed\Participant; +use OCA\Spreed\Signaling\BackendNotifier; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Http\Client\IClientService; +use OCP\ILogger; +use OCP\IUser; +use OCP\Security\IHasher; + +class CustomBackendNotifier extends BackendNotifier { + + private $lastRequest; + + public function getLastRequest() { + return $this->lastRequest; + } + + public function clearLastRequest() { + $this->lastRequest = null; + } + + protected function doRequest($url, $params) { + $this->lastRequest = [ + 'url' => $url, + 'params' => $params, + ]; + } + +} + +class CustomApplication extends Application { + + private $notifier; + + public function setBackendNotifier($notifier) { + $this->notifier = $notifier; + } + + protected function getBackendNotifier() { + return $this->notifier; + } + +} + +/** + * @group DB + */ +class BackendNotifierTest extends \Test\TestCase { + + /** @var OCA\Spreed\Config */ + private $config; + + /** @var ISecureRandom */ + private $secureRandom; + + /** @var CustomBackendNotifier */ + private $controller; + + /** @var Manager */ + private $manager; + + /** @var string */ + private $userId; + + public function setUp() { + parent::setUp(); + // Make sure necessary database tables are set up. + \OC_App::updateApp('spreed'); + + $this->userId = 'testUser'; + $this->secureRandom = \OC::$server->getSecureRandom(); + $timeFactory = $this->createMock(ITimeFactory::class); + $config = \OC::$server->getConfig(); + $this->signalingSecret = 'the-signaling-secret'; + $this->baseUrl = 'https://localhost/signaling'; + $config->setAppValue('spreed', 'signaling_servers', json_encode([ + 'secret' => $this->signalingSecret, + 'servers' => [ + [ + 'server' => $this->baseUrl, + ], + ], + ])); + + $this->config = new Config($config, $this->secureRandom, $timeFactory); + $this->recreateBackendNotifier(); + + $app = new CustomApplication(); + $app->setBackendNotifier($this->controller); + $app->register(); + + \OC::$server->registerService(BackendNotifier::class, function() { + return $this->controller; + }); + + $dbConnection = \OC::$server->getDatabaseConnection(); + $dispatcher = \OC::$server->getEventDispatcher(); + $this->manager = new Manager($dbConnection, $config, $this->secureRandom, $dispatcher, $this->createMock(IHasher::class)); + } + + private function recreateBackendNotifier() { + $this->controller = new CustomBackendNotifier( + $this->config, + $this->createMock(ILogger::class), + $this->createMock(IClientService::class), + $this->secureRandom + ); + } + + private function calculateBackendChecksum($data, $random) { + if (empty($random) || strlen($random) < 32) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->signalingSecret); + return $hash; + } + + private function validateBackendRequest($expectedUrl, $request) { + $this->assertTrue(isset($request)); + $this->assertEquals($expectedUrl, $request['url']); + $headers = $request['params']['headers']; + $this->assertEquals('application/json', $headers['Content-Type']); + $random = $headers['Spreed-Signaling-Random']; + $checksum = $headers['Spreed-Signaling-Checksum']; + $body = $request['params']['body']; + $this->assertEquals($this->calculateBackendChecksum($body, $random), $checksum); + return $body; + } + + public function testRoomInvite() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'invite', + 'invite' => [ + 'userids' => [ + $this->userId, + ], + 'alluserids' => [ + $this->userId, + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomDisinvite() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + $this->controller->clearLastRequest(); + $testUser = $this->createMock(IUser::class); + $testUser + ->method('getUID') + ->willReturn($this->userId); + $room->removeUser($testUser); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'disinvite', + 'disinvite' => [ + 'userids' => [ + $this->userId, + ], + 'alluserids' => [ + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomNameChanged() { + $room = $this->manager->createPublicRoom(); + $room->setName('Test room'); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'update', + 'update' => [ + 'userids' => [ + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomDelete() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + $room->deleteRoom(); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'delete', + 'delete' => [ + 'userids' => [ + $this->userId, + ], + ], + ], json_decode($body, true)); + } + + public function testRoomInCallChanged() { + $room = $this->manager->createPublicRoom(); + $userSession = 'user-session'; + $room->addUsers([ + 'userId' => $this->userId, + 'sessionId' => $userSession, + ]); + $room->changeInCall($userSession, true); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => true, + 'changed' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + 'userId' => $this->userId, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + ], + ], + ], + ], json_decode($body, true)); + + $this->controller->clearLastRequest(); + $guestSession = $room->enterRoomAsGuest(''); + $room->changeInCall($guestSession, true); + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => true, + 'changed' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + ], + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + ], + ], json_decode($body, true)); + + $this->controller->clearLastRequest(); + $room->changeInCall($userSession, false); + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => false, + 'changed' => [ + [ + 'inCall' => false, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + 'userId' => $this->userId, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + ], + ], json_decode($body, true)); + } + +}