diff --git a/appinfo/info.xml b/appinfo/info.xml index 2347e08c067..c705ca909eb 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m ]]> - 12.0.0-alpha.1 + 12.0.0-alpha.2 agpl Daniel Calviño Sánchez diff --git a/appinfo/routes.php b/appinfo/routes.php index 83f76f137cd..13dc7f1f436 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -344,6 +344,15 @@ 'token' => '^[a-z0-9]{4,30}$', ], ], + [ + 'name' => 'Room#setAttendeePublishingPermissions', + 'url' => '/api/{apiVersion}/room/{token}/attendees/publishing-permissions', + 'verb' => 'PUT', + 'requirements' => [ + 'apiVersion' => 'v(4)', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], [ 'name' => 'Room#joinRoom', 'url' => '/api/{apiVersion}/room/{token}/participants/active', diff --git a/docs/capabilities.md b/docs/capabilities.md index ee36a552ad4..0e4765fcdaf 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -77,3 +77,4 @@ title: Capabilities * `signaling-v3` - Whether signaling API v3 is available. This also means that v1 and v2 are **not** available anymore. The TURN and STUN server data is now returning all defined servers instead of a random one. Multiple entries for the same server are combined and using the urls array correctly now. * `geo-location-sharing` - Whether the `geo-location` rich object is defined and can be shared to the rich-object sharing endpoint. * `voice-message-sharing` - Shared files can be flagged as voice messages and are then presented differently in the interface +* `publishing-permissions` - Whether the publishing permissions can be set for the attendees. diff --git a/docs/constants.md b/docs/constants.md index 62c3291b845..0946ee557b4 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -57,6 +57,12 @@ title: Constants * `guests` - Guest without a login * `emails` - A guest invited by email address +### Attendee publishing permissions +* `0` None +* `1` Audio +* `2` Video +* `4` Screensharing + ### Actor types of chat messages * `users` - Logged-in users * `guests` - Guest users (attendee type `guests` and `emails`) diff --git a/docs/conversation.md b/docs/conversation.md index d2d139d1ea3..4c52215cb68 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -37,6 +37,7 @@ `attendeePin` | string | v3 | | Unique dial-in authentication code for this user, when the conversation has SIP enabled (see `sipEnabled` attribute) `actorType` | string | v3 | | Currently known `users|guests|emails|groups|circles` `actorId` | string | v3 | | The unique identifier for the given actor type + `publishingPermissions` | int | v4 | Publishing permissions for the current participant (see [constants list](constants.md#attendee-publishing-permissions)) `participantInCall` | bool | v1 | v2 | **Removed:** use `participantFlags` instead `participantFlags` | int | v1 | | "In call" flags of the user's session making the request (only available with `in-call-flags` capability) `readOnly` | int | v1 | | Read-only state for the current user (only available with `read-only-rooms` capability) diff --git a/docs/participant.md b/docs/participant.md index 336f6bf4b60..c2dfab1b2df 100644 --- a/docs/participant.md +++ b/docs/participant.md @@ -34,6 +34,7 @@ `participantType` | int | v1 | | Permissions level of the participant (see [constants list](constants.md#participant-types)) `lastPing` | int | v1 | | Timestamp of the last ping of the user (should be used for sorting) `inCall` | int | v1 | | Call flags the user joined with (see [constants list](constants.md#participant-in-call-flag)) + `publishingPermissions` | int | v4 | Publishing permissions for the participant (see [constants list](constants.md#attendee-publishing-permissions)) `sessionId` | string | v1 | v4 | `'0'` if not connected, otherwise a 512 character long string `sessionIds` | array | v4 | | array of session ids, each are 512 character long strings, or empty if no session `status` | string | v2 | | Optional: Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation @@ -190,6 +191,25 @@ + `404 Not Found` When the conversation could not be found for the participant + `404 Not Found` When the participant to demote could not be found +## Set publishing permissions for an attendee + +* Method: `PUT` +* Endpoint: `/room/{token}/attendees/publishing-permissions` +* Data: + + field | type | Description + ---|---|--- + `attendeeId` | int | Attendee id can be used for guests and users + `state` | int | New state for the attendee, see [constants list](constants.md#attendee-publishing-permissions) + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the conversation type does not support setting publishing permissions (only group and public conversations) + + `403 Forbidden` When the current user is not a moderator, owner or guest moderator + + `404 Not Found` When the conversation could not be found for the participant + + `404 Not Found` When the attendee to set publishing permissions could not be found + ## Get a participant by their pin Note: This is only allowed with validate SIP bridge requests diff --git a/lib/Capabilities.php b/lib/Capabilities.php index ee4190d9f56..5421e171205 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -93,6 +93,7 @@ public function getCapabilities(): array { 'geo-location-sharing', 'voice-message-sharing', 'signaling-v3', + 'publishing-permissions', ], 'config' => [ 'attachments' => [ diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 8bed261aaa1..01eb2d4acb4 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -372,6 +372,7 @@ protected function formatRoomV4(Room $room, ?Participant $currentParticipant, bo 'actorType' => '', 'actorId' => '', 'attendeeId' => 0, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, 'canEnableSIP' => false, 'attendeePin' => '', 'description' => '', @@ -437,6 +438,7 @@ protected function formatRoomV4(Room $room, ?Participant $currentParticipant, bo 'actorType' => $attendee->getActorType(), 'actorId' => $attendee->getActorId(), 'attendeeId' => $attendee->getId(), + 'publishingPermissions' => $attendee->getPublishingPermissions(), 'description' => $room->getDescription(), 'listable' => $room->getListable(), ]); @@ -906,6 +908,7 @@ public function getParticipants(bool $includeStatus = false): DataResponse { 'actorId' => $participant->getAttendee()->getActorId(), 'actorType' => $participant->getAttendee()->getActorType(), 'displayName' => $participant->getAttendee()->getActorId(), + 'publishingPermissions' => $participant->getAttendee()->getPublishingPermissions(), 'attendeePin' => '', ]; if ($this->talkConfig->isSIPConfigured() @@ -1428,6 +1431,30 @@ protected function changeParticipantType(int $attendeeId, bool $promote): DataR return new DataResponse(); } + /** + * @PublicPage + * @RequireModeratorParticipant + * + * @param int $attendeeId + * @param int $state + * @return DataResponse + */ + public function setAttendeePublishingPermissions(int $attendeeId, int $state): DataResponse { + try { + $targetParticipant = $this->room->getParticipantByAttendeeId($attendeeId); + } catch (ParticipantNotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if ($this->room->getType() === Room::ONE_TO_ONE_CALL) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $this->participantService->updatePublishingPermissions($this->room, $targetParticipant, $state); + + return new DataResponse(); + } + /** * @NoAdminRequired * @RequireModeratorParticipant diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 166e010f336..8b85b8f83f2 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -399,6 +399,7 @@ protected function getUsersInRoom(Room $room, int $pingTimestamp): array { 'lastPing' => $session->getLastPing(), 'sessionId' => $session->getSessionId(), 'inCall' => $session->getInCall(), + 'publishingPermissions' => $participant->getAttendee()->getPublishingPermissions(), ]; } @@ -645,9 +646,17 @@ private function backendRoom(array $roomRequest): DataResponse { } } - $permissions = ['publish-media', 'publish-screen']; - if ($participant instanceof Participant && $participant->hasModeratorPermissions(false)) { - $permissions[] = 'control'; + $permissions = []; + if ($participant instanceof Participant) { + if ($participant->getAttendee()->getPublishingPermissions() & (Attendee::PUBLISHING_PERMISSIONS_AUDIO | Attendee::PUBLISHING_PERMISSIONS_VIDEO)) { + $permissions[] = 'publish-media'; + } + if ($participant->getAttendee()->getPublishingPermissions() & Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING) { + $permissions[] = 'publish-screen'; + } + if ($participant->hasModeratorPermissions(false)) { + $permissions[] = 'control'; + } } $event = new SignalingEvent($room, $participant, $action); diff --git a/lib/Migration/Version12000Date20210528100404.php b/lib/Migration/Version12000Date20210528100404.php new file mode 100644 index 00000000000..e3f4a5802fc --- /dev/null +++ b/lib/Migration/Version12000Date20210528100404.php @@ -0,0 +1,58 @@ + + * + * @author Daniel Calviño Sánchez + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use Doctrine\DBAL\Types\Types; +use OCA\Talk\Model\Attendee; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version12000Date20210528100404 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('talk_attendees'); + if (!$table->hasColumn('publishing_permissions')) { + $table->addColumn('publishing_permissions', Types::INTEGER, [ + 'default' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); + + return $schema; + } + + return null; + } +} diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 7766681cd39..5a758d4ae53 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -49,6 +49,8 @@ * @method int getLastMentionMessage() * @method void setReadPrivacy(int $readPrivacy) * @method int getReadPrivacy() + * @method void setPublishingPermissions(int $publishingPermissions) + * @method int getPublishingPermissions() */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; @@ -57,6 +59,12 @@ class Attendee extends Entity { public const ACTOR_EMAILS = 'emails'; public const ACTOR_CIRCLES = 'circles'; + public const PUBLISHING_PERMISSIONS_NONE = 0; + public const PUBLISHING_PERMISSIONS_AUDIO = 1; + public const PUBLISHING_PERMISSIONS_VIDEO = 2; + public const PUBLISHING_PERMISSIONS_SCREENSHARING = 4; + public const PUBLISHING_PERMISSIONS_ALL = 7; + /** @var int */ protected $roomId; @@ -93,6 +101,9 @@ class Attendee extends Entity { /** @var int */ protected $readPrivacy; + /** @var int */ + protected $publishingPermissions; + public function __construct() { $this->addType('roomId', 'int'); $this->addType('actorType', 'string'); @@ -106,6 +117,7 @@ public function __construct() { $this->addType('lastReadMessage', 'int'); $this->addType('lastMentionMessage', 'int'); $this->addType('readPrivacy', 'int'); + $this->addType('publishingPermissions', 'int'); } public function getDisplayName(): string { @@ -130,6 +142,7 @@ public function asArray(): array { 'last_read_message' => $this->getLastReadMessage(), 'last_mention_message' => $this->getLastMentionMessage(), 'read_privacy' => $this->getReadPrivacy(), + 'publishing_permissions' => $this->getPublishingPermissions(), ]; } } diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 1b928fc9310..3e8586389ee 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -152,6 +152,7 @@ public function createAttendeeFromRow(array $row): Attendee { 'last_read_message' => (int) $row['last_read_message'], 'last_mention_message' => (int) $row['last_mention_message'], 'read_privacy' => (int) $row['read_privacy'], + 'publishing_permissions' => (int) $row['publishing_permissions'], ]); } } diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 3bf0463c819..dfcab988ad9 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -69,6 +69,7 @@ public function selectAttendeesTable(IQueryBuilder $query, string $alias = 'a'): ->addSelect($alias . 'last_read_message') ->addSelect($alias . 'last_mention_message') ->addSelect($alias . 'read_privacy') + ->addSelect($alias . 'publishing_permissions') ->selectAlias($alias . 'id', 'a_id'); } diff --git a/lib/Room.php b/lib/Room.php index 41b83f2ba58..42d99fd1d18 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -109,6 +109,8 @@ class Room { public const EVENT_AFTER_USERS_ADD = self::class . '::postAddUsers'; public const EVENT_BEFORE_PARTICIPANT_TYPE_SET = self::class . '::preSetParticipantType'; public const EVENT_AFTER_PARTICIPANT_TYPE_SET = self::class . '::postSetParticipantType'; + public const EVENT_BEFORE_PARTICIPANT_PUBLISHING_PERMISSIONS_SET = self::class . '::preSetParticipantPublishingPermissions'; + public const EVENT_AFTER_PARTICIPANT_PUBLISHING_PERMISSIONS_SET = self::class . '::postSetParticipantPublishingPermissions'; public const EVENT_BEFORE_USER_REMOVE = self::class . '::preRemoveUser'; public const EVENT_AFTER_USER_REMOVE = self::class . '::postRemoveUser'; public const EVENT_BEFORE_PARTICIPANT_REMOVE = self::class . '::preRemoveBySession'; diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index a502af98d1a..2d1762dcc34 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -125,6 +125,25 @@ public function updateParticipantType(Room $room, Participant $participant, int $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event); } + public function updatePublishingPermissions(Room $room, Participant $participant, int $newState): void { + $attendee = $participant->getAttendee(); + + if ($attendee->getActorType() === Attendee::ACTOR_GROUPS || $attendee->getActorType() === Attendee::ACTOR_CIRCLES) { + // Can not set publishing permissions for those actor types + return; + } + + $oldState = $attendee->getPublishingPermissions(); + + $event = new ModifyParticipantEvent($room, $participant, 'publishingPermissions', $newState, $oldState); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_PUBLISHING_PERMISSIONS_SET, $event); + + $attendee->setPublishingPermissions($newState); + $this->attendeeMapper->update($attendee); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_PUBLISHING_PERMISSIONS_SET, $event); + } + public function updateLastReadMessage(Participant $participant, int $lastReadMessage): void { $attendee = $participant->getAttendee(); $attendee->setLastReadMessage($lastReadMessage); @@ -655,6 +674,14 @@ public function changeInCall(Room $room, Participant $participant, int $flags): return; } + $publishingPermissions = $participant->getAttendee()->getPublishingPermissions(); + if (!($publishingPermissions & Attendee::PUBLISHING_PERMISSIONS_AUDIO)) { + $flags &= ~Participant::FLAG_WITH_AUDIO; + } + if (!($publishingPermissions & Attendee::PUBLISHING_PERMISSIONS_VIDEO)) { + $flags &= ~Participant::FLAG_WITH_VIDEO; + } + $event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall()); if ($flags !== Participant::FLAG_DISCONNECTED) { $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_JOIN_CALL, $event); @@ -692,6 +719,14 @@ public function updateCallFlags(Room $room, Participant $participant, int $flags throw new \InvalidArgumentException('Invalid flags'); } + $publishingPermissions = $participant->getAttendee()->getPublishingPermissions(); + if (!($publishingPermissions & Attendee::PUBLISHING_PERMISSIONS_AUDIO)) { + $flags &= ~Participant::FLAG_WITH_AUDIO; + } + if (!($publishingPermissions & Attendee::PUBLISHING_PERMISSIONS_VIDEO)) { + $flags &= ~Participant::FLAG_WITH_VIDEO; + } + $event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall()); $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS, $event); diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 50f906c87a2..2338f6b0d66 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -264,6 +264,7 @@ public function participantsModified(Room $room, array $sessionIds): void { 'lastPing' => 0, 'sessionId' => '0', 'participantType' => $attendee->getParticipantType(), + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, ]; if ($attendee->getActorType() === Attendee::ACTOR_USERS) { $data['userId'] = $attendee->getActorId(); @@ -274,10 +275,17 @@ public function participantsModified(Room $room, array $sessionIds): void { $data['inCall'] = $session->getInCall(); $data['lastPing'] = $session->getLastPing(); $data['sessionId'] = $session->getSessionId(); + $data['publishingPermissions'] = $attendee->getPublishingPermissions(); $users[] = $data; if (\in_array($session->getSessionId(), $sessionIds, true)) { - $data['permissions'] = ['publish-media', 'publish-screen']; + $data['permissions'] = []; + if ($attendee->getPublishingPermissions() & (Attendee::PUBLISHING_PERMISSIONS_AUDIO | Attendee::PUBLISHING_PERMISSIONS_VIDEO)) { + $data['permissions'][] = 'publish-media'; + } + if ($attendee->getPublishingPermissions() & Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING) { + $data['permissions'][] = 'publish-screen'; + } if ($attendee->getParticipantType() === Participant::OWNER || $attendee->getParticipantType() === Participant::MODERATOR) { $data['permissions'][] = 'control'; diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 9ebec18bdc0..59188abc09d 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -83,6 +83,7 @@ protected static function registerInternalSignaling(IEventDispatcher $dispatcher $dispatcher->addListener(Room::EVENT_BEFORE_USER_REMOVE, $listener); $dispatcher->addListener(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $listener); $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DISCONNECT, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PUBLISHING_PERMISSIONS_SET, $listener); $listener = static function (RoomEvent $event) { $room = $event->getRoom(); @@ -130,7 +131,8 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher // "participantsModified" once the clients no longer expect a // "roomModified" message for participant type changes. $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, static function (ModifyParticipantEvent $event) { + + $listener = static function (ModifyParticipantEvent $event) { if (self::isUsingInternalSignaling()) { return; } @@ -151,7 +153,10 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher } $notifier->participantsModified($event->getRoom(), $sessionIds); - }); + }; + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PUBLISHING_PERMISSIONS_SET, $listener); + $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, static function (RoomEvent $event) { if (self::isUsingInternalSignaling()) { return; diff --git a/src/PublicShareAuthSidebar.vue b/src/PublicShareAuthSidebar.vue index 33f4d588cf3..1db9a79f6aa 100644 --- a/src/PublicShareAuthSidebar.vue +++ b/src/PublicShareAuthSidebar.vue @@ -40,7 +40,6 @@ import { getCurrentUser } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' import CallView from './components/CallView/CallView' import ChatView from './components/ChatView' -import { PARTICIPANT } from './constants' import { EventBus } from './services/EventBus' import { leaveConversationSync, @@ -153,7 +152,6 @@ export default { await this.$store.dispatch('joinCall', { token: this.token, participantIdentifier: this.$store.getters.getParticipantIdentifier(), - flags: PARTICIPANT.CALL_FLAG.IN_CALL, }) }, diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index c289a2129c0..5f470f3dbf1 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -30,6 +30,7 @@ { }, }) - joinCall.mockResolvedValue() + // The requested flags and the actual flags can be different if some + // media device is not available. + const actualFlags = PARTICIPANT.CALL_FLAG.WITH_AUDIO + joinCall.mockResolvedValue(actualFlags) expect(store.getters.isInCall(TOKEN)).toBe(false) expect(store.getters.isConnecting(TOKEN)).toBe(false) - const flags = PARTICIPANT.CALL_FLAG.WITH_VIDEO + const flags = PARTICIPANT.CALL_FLAG.WITH_AUDIO | PARTICIPANT.CALL_FLAG.WITH_VIDEO await store.dispatch('joinCall', { token: TOKEN, participantIdentifier: { @@ -356,7 +359,7 @@ describe('participantsStore', () => { { attendeeId: 1, sessionId: 'session-id-1', - inCall: flags, + inCall: actualFlags, participantType: PARTICIPANT.TYPE.USER, }, ]) @@ -381,12 +384,15 @@ describe('participantsStore', () => { }, }) - joinCall.mockResolvedValue() + // The requested flags and the actual flags can be different if some + // media device is not available. + const actualFlags = PARTICIPANT.CALL_FLAG.WITH_AUDIO + joinCall.mockResolvedValue(actualFlags) expect(store.getters.isInCall(TOKEN)).toBe(false) expect(store.getters.isConnecting(TOKEN)).toBe(false) - const flags = PARTICIPANT.CALL_FLAG.WITH_VIDEO + const flags = PARTICIPANT.CALL_FLAG.WITH_AUDIO | PARTICIPANT.CALL_FLAG.WITH_VIDEO await store.dispatch('joinCall', { token: TOKEN, participantIdentifier: { @@ -402,7 +408,7 @@ describe('participantsStore', () => { { attendeeId: 1, sessionId: 'session-id-1', - inCall: flags, + inCall: actualFlags, participantType: PARTICIPANT.TYPE.USER, }, ]) diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index 73d2815887b..f2591455f84 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -116,7 +116,7 @@ function startCall(signaling, configuration) { } signaling.joinCall(pendingJoinCallToken, flags).then(() => { - startedCall() + startedCall(flags) }).catch(error => { failedToStartCall(error) }) @@ -127,18 +127,9 @@ function setupWebRtc() { return } - const _signaling = signaling - - webRtc = initWebRtc(_signaling, callParticipantCollection, localCallParticipantModel) + webRtc = initWebRtc(signaling, callParticipantCollection, localCallParticipantModel) localCallParticipantModel.setWebRtc(webRtc) localMediaModel.setWebRtc(webRtc) - - webRtc.on('localMediaStarted', (configuration) => { - startCall(_signaling, configuration) - }) - webRtc.on('localMediaError', () => { - startCall(_signaling, null) - }) } /** @@ -159,9 +150,11 @@ async function signalingJoinConversation(token, sessionId) { * Join the call of the given conversation * * @param {string} token Conversation to join the call - * @returns {Promise} + * @param {int} flags Bitwise combination of PARTICIPANT.CALL_FLAG + * @returns {Promise} Resolved with the actual flags based on the + * available media */ -async function signalingJoinCall(token) { +async function signalingJoinCall(token, flags) { if (tokensInSignaling[token]) { pendingJoinCallToken = token @@ -175,11 +168,47 @@ async function signalingJoinCall(token) { callAnalyzer = new CallAnalyzer(localMediaModel, null, callParticipantCollection) } + const _signaling = signaling + return new Promise((resolve, reject) => { startedCall = resolve failedToStartCall = reject - webRtc.startMedia(token) + // The previous state might be wiped after the media is started, so + // it should be saved now. + const enableAudio = !localStorage.getItem('audioDisabled_' + token) + const enableVideo = !localStorage.getItem('videoDisabled_' + token) + + const startCallOnceLocalMediaStarted = (configuration) => { + webRtc.off('localMediaStarted', startCallOnceLocalMediaStarted) + webRtc.off('localMediaError', startCallOnceLocalMediaError) + + if (enableAudio) { + localMediaModel.enableAudio() + } else { + localMediaModel.disableAudio() + } + if (enableVideo) { + localMediaModel.enableVideo() + } else { + localMediaModel.disableVideo() + } + + startCall(_signaling, configuration) + } + const startCallOnceLocalMediaError = () => { + webRtc.off('localMediaStarted', startCallOnceLocalMediaStarted) + webRtc.off('localMediaError', startCallOnceLocalMediaError) + + startCall(_signaling, null) + } + + // ".once" can not be used, as both handlers need to be removed when + // just one of them is executed. + webRtc.on('localMediaStarted', startCallOnceLocalMediaStarted) + webRtc.on('localMediaError', startCallOnceLocalMediaError) + + webRtc.startMedia(token, flags) }) } } diff --git a/src/utils/webrtc/models/LocalMediaModel.js b/src/utils/webrtc/models/LocalMediaModel.js index b17b579a2e4..6fcb535d4ae 100644 --- a/src/utils/webrtc/models/LocalMediaModel.js +++ b/src/utils/webrtc/models/LocalMediaModel.js @@ -206,7 +206,7 @@ LocalMediaModel.prototype = { _setInitialMediaState: function(configuration) { if (configuration.audio !== false) { this.set('audioAvailable', true) - if (!localStorage.getItem('audioDisabled_' + this.get('token')) || this.get('audioEnabled')) { + if (this.get('audioEnabled')) { this.enableAudio() } else { this.disableAudio() @@ -218,7 +218,7 @@ LocalMediaModel.prototype = { if (configuration.video !== false) { this.set('videoAvailable', true) - if (!localStorage.getItem('videoDisabled_' + this.get('token')) || this.get('videoEnabled')) { + if (this.get('videoEnabled')) { this.enableVideo() } else { this.disableVideo() @@ -274,6 +274,11 @@ LocalMediaModel.prototype = { } this.set('localStream', null) + + this.set('audioEnabled', false) + this.set('audioAvailable', false) + this.set('videoEnabled', false) + this.set('videoAvailable', false) }, _handleAudioOn: function() { diff --git a/src/utils/webrtc/simplewebrtc/localmedia.js b/src/utils/webrtc/simplewebrtc/localmedia.js index bcb69904db8..ea678b1cf49 100644 --- a/src/utils/webrtc/simplewebrtc/localmedia.js +++ b/src/utils/webrtc/simplewebrtc/localmedia.js @@ -31,10 +31,6 @@ function LocalMedia(opts) { const config = this.config = { detectSpeakingEvents: false, audioFallback: false, - media: { - audio: true, - video: true, - }, harkOptions: null, logger: mockconsole, } @@ -53,6 +49,8 @@ function LocalMedia(opts) { this._audioEnabled = true this._videoEnabled = true + this._localMediaActive = false + this.localStreams = [] this._audioMonitorStreams = [] this.localScreens = [] @@ -121,9 +119,36 @@ const cloneLinkedStream = function(stream) { return linkedStream } +/** + * Returns whether the local media is active or not. + * + * The local media is active if it has been started and not stopped yet, even if + * no media was available when started. An active local media will automatically + * react to changes in the selected media devices. + * + * @returns {bool} true if the local media is active, false otherwise + */ +LocalMedia.prototype.isLocalMediaActive = function() { + return this._localMediaActive +} + LocalMedia.prototype.start = function(mediaConstraints, cb, context) { const self = this - const constraints = mediaConstraints || this.config.media + const constraints = mediaConstraints || { audio: true, video: true } + + // If local media is started with neither audio nor video the local media + // will not be active (it will not react to changes in the selected media + // devices). It is just a special case in which starting succeeds with a null + // stream. + if (!constraints.audio && !constraints.video) { + self.emit('localStream', constraints, null) + + if (cb) { + return cb(null, null, constraints) + } + + return + } if (!webrtcIndex.mediaDevicesManager.isSupported()) { const error = new Error('MediaStreamError') @@ -186,8 +211,10 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) { webrtcIndex.mediaDevicesManager.on('change:audioInputId', self._handleAudioInputIdChangedBound) webrtcIndex.mediaDevicesManager.on('change:videoInputId', self._handleVideoInputIdChangedBound) + self._localMediaActive = true + if (cb) { - return cb(null, stream) + return cb(null, stream, constraints) } }).catch(function(err) { // Fallback for users without a camera or with a camera that can not be @@ -204,6 +231,8 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) { webrtcIndex.mediaDevicesManager.on('change:audioInputId', self._handleAudioInputIdChangedBound) webrtcIndex.mediaDevicesManager.on('change:videoInputId', self._handleVideoInputIdChangedBound) + self._localMediaActive = true + if (cb) { return cb(err, null) } @@ -493,6 +522,8 @@ LocalMedia.prototype.stop = function(stream) { webrtcIndex.mediaDevicesManager.off('change:audioInputId', this._handleAudioInputIdChangedBound) webrtcIndex.mediaDevicesManager.off('change:videoInputId', this._handleVideoInputIdChangedBound) + + this._localMediaActive = false } LocalMedia.prototype.stopStream = function(stream) { diff --git a/src/utils/webrtc/simplewebrtc/simplewebrtc.js b/src/utils/webrtc/simplewebrtc/simplewebrtc.js index ab919828e5e..86d37c62a0e 100644 --- a/src/utils/webrtc/simplewebrtc/simplewebrtc.js +++ b/src/utils/webrtc/simplewebrtc/simplewebrtc.js @@ -20,10 +20,6 @@ function SimpleWebRTC(opts) { autoRemoveVideos: true, adjustPeerVolume: false, peerVolumeWhenSpeaking: 0.25, - media: { - video: true, - audio: true, - }, receiveMedia: { offerToReceiveAudio: 1, offerToReceiveVideo: 1, @@ -329,9 +325,9 @@ SimpleWebRTC.prototype.setVolumeForAll = function(volume) { }) } -SimpleWebRTC.prototype.joinCall = function(name) { +SimpleWebRTC.prototype.joinCall = function(name, mediaConstraints) { if (this.config.autoRequestMedia) { - this.startLocalVideo() + this.startLocalVideo(mediaConstraints) } this.roomName = name this.emit('joinedRoom', name) @@ -345,17 +341,13 @@ SimpleWebRTC.prototype.getEl = function(idOrEl) { } } -SimpleWebRTC.prototype.startLocalVideo = function() { +SimpleWebRTC.prototype.startLocalVideo = function(mediaConstraints) { const self = this - const constraints = { - audio: true, - video: true, - } - this.webrtc.start(constraints, function(err, stream) { + this.webrtc.start(mediaConstraints, function(err, stream, actualConstraints) { if (err) { self.emit('localMediaError', err) } else { - self.emit('localMediaStarted', constraints) + self.emit('localMediaStarted', actualConstraints) const localVideoContainer = self.getLocalVideoContainer() if (localVideoContainer) { diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index 98e47ea9636..39dc7e65bc1 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -525,10 +525,6 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local remoteVideosEl: '', autoRequestMedia: true, debug: false, - media: { - audio: true, - video: true, - }, autoAdjustMic: false, audioFallback: true, detectSpeakingEvents: true, @@ -552,10 +548,17 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local webrtc.leaveCall() }) - webrtc.startMedia = function(token) { + webrtc.startMedia = function(token, flags) { startedWithMedia = undefined - webrtc.joinCall(token) + // If no flags are provided try to enable both audio and video. + // Otherwise, try to enable only that allowed by the flags. + const mediaConstraints = { + audio: !flags || !!(flags & PARTICIPANT.CALL_FLAG.WITH_AUDIO), + video: !flags || !!(flags & PARTICIPANT.CALL_FLAG.WITH_VIDEO), + } + + webrtc.joinCall(token, mediaConstraints) } const sendDataChannelToAll = function(channel, message, payload) { @@ -745,6 +748,91 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local }) } + const reconnectOnPublishingPermissionsChange = (users) => { + const currentParticipant = users.find(user => { + const sessionId = user.sessionId || user.sessionid + return sessionId === signaling.getSessionId() + }) + + if (!currentParticipant) { + return + } + + if (!currentParticipant.inCall) { + return + } + + if (currentParticipant.publishingPermissions === undefined) { + return + } + + if (currentParticipant.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.ALL && webrtc.webrtc.isLocalMediaActive()) { + return + } + + if (currentParticipant.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.NONE && !webrtc.webrtc.isLocalMediaActive()) { + return + } + + if (currentParticipant.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.NONE) { + startedWithMedia = undefined + + webrtc.stopLocalVideo() + + // If the MCU is used and there is no sending peer there is no need + // to force a reconnection, as there will be no connection that + // needs to be stopped. + if (!signaling.hasFeature('mcu') || ownPeer) { + forceReconnect(signaling, PARTICIPANT.CALL_FLAG.IN_CALL) + } + + return + } + + const forceReconnectOnceLocalMediaStarted = (constraints) => { + webrtc.off('localMediaStarted', forceReconnectOnceLocalMediaStarted) + webrtc.off('localMediaError', forceReconnectOnceLocalMediaError) + + startedWithMedia = true + + let flags = PARTICIPANT.CALL_FLAG.IN_CALL + if (constraints) { + if (constraints.audio) { + flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO + } + if (constraints.video && signaling.getSendVideoIfAvailable()) { + flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO + } + } + + forceReconnect(signaling, flags) + } + const forceReconnectOnceLocalMediaError = () => { + webrtc.off('localMediaStarted', forceReconnectOnceLocalMediaStarted) + webrtc.off('localMediaError', forceReconnectOnceLocalMediaError) + + startedWithMedia = false + + // If the media fails to start there will be no media, so no need to + // reconnect. A reconnection will happen once the user selects a + // different device. + } + + webrtc.on('localMediaStarted', forceReconnectOnceLocalMediaStarted) + webrtc.on('localMediaError', forceReconnectOnceLocalMediaError) + + startedWithMedia = undefined + + webrtc.startLocalVideo() + } + + signaling.on('usersInRoom', function(users) { + reconnectOnPublishingPermissionsChange(users) + }) + signaling.on('usersChanged', function(users) { + reconnectOnPublishingPermissionsChange(users) + }) + webrtc.on('createdPeer', function(peer) { console.debug('Peer created', peer) diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 57fa6a9d133..e3acd1ec2de 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -392,6 +392,9 @@ public function userSeesAttendeesInRoom(string $user, string $identifier, int $s if (isset($expectedKeys['attendeePin'])) { $data['attendeePin'] = $attendee['attendeePin'] ? '**PIN**' : ''; } + if (isset($expectedKeys['publishingPermissions'])) { + $data['publishingPermissions'] = (string) $attendee['publishingPermissions']; + } if (!isset(self::$userToAttendeeId[$attendee['actorType']])) { self::$userToAttendeeId[$attendee['actorType']] = []; @@ -408,6 +411,9 @@ public function userSeesAttendeesInRoom(string $user, string $identifier, int $s if (isset($attendee['participantType'])) { $attendee['participantType'] = (string)$this->mapParticipantTypeTestInput($attendee['participantType']); } + if (isset($attendee['publishingPermissions'])) { + $attendee['publishingPermissions'] = (string)$this->mapPublishingPermissionsTestInput($attendee['publishingPermissions']); + } return $attendee; }, $formData->getHash()); @@ -468,6 +474,22 @@ private function mapParticipantTypeTestInput($participantType) { Assert::fail('Invalid test input value for participant type'); } + private function mapPublishingPermissionsTestInput($publishingPermissions) { + if (is_numeric($publishingPermissions)) { + return $publishingPermissions; + } + + switch ($publishingPermissions) { + case 'NONE': return 0; + case 'AUDIO': return 1; + case 'VIDEO': return 2; + case 'SCREENSHARING': return 4; + case 'ALL': return 7; + } + + Assert::fail('Invalid test input value for publishing permissions'); + } + /** * @param string $guest * @param string $isOrNotParticipant @@ -1055,6 +1077,52 @@ public function userPromoteDemoteInRoom(string $user, string $isPromotion, strin $this->assertStatusCode($this->response, $statusCode); } + /** + * @When /^user "([^"]*)" sets publishing permissions for "([^"]*)" in room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ + * + * @param string $user + * @param string $identifier + * @param string $publishingPermissionsString + * @param int $statusCode + * @param string $apiVersion + */ + public function userSetsPublishingPermissionsForInRoomTo(string $user, string $participant, string $identifier, string $publishingPermissionsString, int $statusCode, string $apiVersion): void { + if ($participant === 'stranger') { + $attendeeId = 123456789; + } elseif (strpos($participant, 'guest') === 0) { + $sessionId = self::$userToSessionId[$participant]; + $attendeeId = $this->getAttendeeId('guests', sha1($sessionId), $identifier, $statusCode === 200 ? $user : null); + } else { + $attendeeId = $this->getAttendeeId('users', $participant, $identifier, $statusCode === 200 ? $user : null); + } + + if ($publishingPermissionsString === 'NONE') { + $publishingPermissions = 0; + } elseif ($publishingPermissionsString === 'AUDIO') { + $publishingPermissions = 1; + } elseif ($publishingPermissionsString === 'VIDEO') { + $publishingPermissions = 2; + } elseif ($publishingPermissionsString === 'SCREENSHARING') { + $publishingPermissions = 4; + } elseif ($publishingPermissionsString === 'ALL') { + $publishingPermissions = 7; + } else { + Assert::fail('Invalid publishing permissions'); + } + + $requestParameters = [ + ['attendeeId', $attendeeId], + ['state', $publishingPermissions], + ]; + + $this->setCurrentUser($user); + $this->sendRequest( + 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/attendees/publishing-permissions', + new TableNode($requestParameters) + ); + $this->assertStatusCode($this->response, $statusCode); + } + /** * @Then /^user "([^"]*)" joins call "([^"]*)" with (\d+) \((v4)\)$/ * diff --git a/tests/integration/features/conversation-2/set-publishing-permissions.feature b/tests/integration/features/conversation-2/set-publishing-permissions.feature new file mode 100644 index 00000000000..b75430684e5 --- /dev/null +++ b/tests/integration/features/conversation-2/set-publishing-permissions.feature @@ -0,0 +1,439 @@ +Feature: set-publishing-permissions + Background: + Given user "owner" exists + Given user "moderator" exists + Given user "invited user" exists + Given user "not invited user" exists + Given user "not invited but joined user" exists + Given user "not joined user" exists + + Scenario: owner can not set publishing permissions in one-to-one room + Given user "owner" creates room "one-to-one room" (v4) + | roomType | 1 | + | invite | moderator | + And user "owner" loads attendees attendee ids in room "one-to-one room" (v4) + When user "owner" sets publishing permissions for "owner" in room "one-to-one room" to "NONE" with 400 (v4) + And user "owner" sets publishing permissions for "moderator" in room "one-to-one room" to "NONE" with 400 (v4) + And user "moderator" sets publishing permissions for "owner" in room "one-to-one room" to "NONE" with 400 (v4) + And user "moderator" sets publishing permissions for "moderator" in room "one-to-one room" to "NONE" with 400 (v4) + Then user "owner" sees the following attendees in room "one-to-one room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + And user "moderator" sees the following attendees in room "one-to-one room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + + + + Scenario: owner can set publishing permissions in group room + Given user "owner" creates room "group room" (v4) + | roomType | 2 | + | roomName | room | + And user "owner" adds user "moderator" to room "group room" with 200 (v4) + And user "owner" promotes "moderator" in room "group room" with 200 (v4) + And user "owner" adds user "invited user" to room "group room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "group room" (v4) + When user "owner" sets publishing permissions for "owner" in room "group room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "moderator" in room "group room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "invited user" in room "group room" to "NONE" with 200 (v4) + Then user "owner" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + And user "moderator" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + And user "invited user" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + + Scenario: moderator can set publishing permissions in group room + Given user "owner" creates room "group room" (v4) + | roomType | 2 | + | roomName | room | + And user "owner" adds user "moderator" to room "group room" with 200 (v4) + And user "owner" promotes "moderator" in room "group room" with 200 (v4) + And user "owner" adds user "invited user" to room "group room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "group room" (v4) + When user "moderator" sets publishing permissions for "owner" in room "group room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "moderator" in room "group room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "invited user" in room "group room" to "NONE" with 200 (v4) + Then user "owner" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + And user "moderator" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + And user "invited user" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + + Scenario: others can not set publishing permissions in group room + Given user "owner" creates room "group room" (v4) + | roomType | 2 | + | roomName | room | + And user "owner" adds user "moderator" to room "group room" with 200 (v4) + And user "owner" promotes "moderator" in room "group room" with 200 (v4) + And user "owner" adds user "invited user" to room "group room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "group room" (v4) + When user "invited user" sets publishing permissions for "owner" in room "group room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "moderator" in room "group room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "invited user" in room "group room" to "NONE" with 403 (v4) + And user "not invited user" sets publishing permissions for "owner" in room "group room" to "NONE" with 404 (v4) + And user "not invited user" sets publishing permissions for "moderator" in room "group room" to "NONE" with 404 (v4) + And user "not invited user" sets publishing permissions for "invited user" in room "group room" to "NONE" with 404 (v4) + # Guest user names in tests must begin with "guest" + And user "guest not joined" sets publishing permissions for "owner" in room "group room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "moderator" in room "group room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "invited user" in room "group room" to "NONE" with 404 (v4) + Then user "owner" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + And user "moderator" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + And user "invited user" sees the following attendees in room "group room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + + + + Scenario: owner can set publishing permissions in public room + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "public room" (v4) + When user "owner" sets publishing permissions for "owner" in room "public room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "moderator" in room "public room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "invited user" in room "public room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 200 (v4) + And user "owner" sets publishing permissions for "guest" in room "public room" to "NONE" with 200 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + + Scenario: moderator can set publishing permissions in public room + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "public room" (v4) + When user "moderator" sets publishing permissions for "owner" in room "public room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "moderator" in room "public room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "invited user" in room "public room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 200 (v4) + And user "moderator" sets publishing permissions for "guest" in room "public room" to "NONE" with 200 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + # Guests can not fetch the participant list + + Scenario: guest moderator can set publishing permissions in public room + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "public room" (v4) + # Guest user names in tests must begin with "guest" + When user "guest moderator" sets publishing permissions for "owner" in room "public room" to "NONE" with 200 (v4) + And user "guest moderator" sets publishing permissions for "moderator" in room "public room" to "NONE" with 200 (v4) + And user "guest moderator" sets publishing permissions for "invited user" in room "public room" to "NONE" with 200 (v4) + And user "guest moderator" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 200 (v4) + And user "guest moderator" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 200 (v4) + And user "guest moderator" sets publishing permissions for "guest" in room "public room" to "NONE" with 200 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | NONE | + | users | moderator | NONE | + | users | invited user | NONE | + | users | not invited but joined user | NONE | + | guests | "guest moderator" | NONE | + | guests | "guest" | NONE | + # Guests can not fetch the participant list + + Scenario: others can not set publishing permissions in public room + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" loads attendees attendee ids in room "public room" (v4) + When user "invited user" sets publishing permissions for "owner" in room "public room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "moderator" in room "public room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "invited user" in room "public room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 403 (v4) + And user "invited user" sets publishing permissions for "guest" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "owner" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "moderator" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "invited user" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 403 (v4) + And user "not invited but joined user" sets publishing permissions for "guest" in room "public room" to "NONE" with 403 (v4) + And user "not joined user" sets publishing permissions for "owner" in room "public room" to "NONE" with 404 (v4) + And user "not joined user" sets publishing permissions for "moderator" in room "public room" to "NONE" with 404 (v4) + And user "not joined user" sets publishing permissions for "invited user" in room "public room" to "NONE" with 404 (v4) + And user "not joined user" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 404 (v4) + And user "not joined user" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 404 (v4) + And user "not joined user" sets publishing permissions for "guest" in room "public room" to "NONE" with 404 (v4) + # Guest user names in tests must begin with "guest" + And user "guest" sets publishing permissions for "owner" in room "public room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "moderator" in room "public room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "invited user" in room "public room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "guest" in room "public room" to "NONE" with 403 (v4) + And user "guest not joined" sets publishing permissions for "owner" in room "public room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "moderator" in room "public room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "invited user" in room "public room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "not invited but joined user" in room "public room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "guest moderator" in room "public room" to "NONE" with 404 (v4) + And user "guest not joined" sets publishing permissions for "guest" in room "public room" to "NONE" with 404 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + | users | not invited but joined user | ALL | + | guests | "guest moderator" | ALL | + | guests | "guest" | ALL | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + | users | not invited but joined user | ALL | + | guests | "guest moderator" | ALL | + | guests | "guest" | ALL | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + | users | not invited but joined user | ALL | + | guests | "guest moderator" | ALL | + | guests | "guest" | ALL | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + | users | not invited but joined user | ALL | + | guests | "guest moderator" | ALL | + | guests | "guest" | ALL | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner | ALL | + | users | moderator | ALL | + | users | invited user | ALL | + | users | not invited but joined user | ALL | + | guests | "guest moderator" | ALL | + | guests | "guest" | ALL | + # Guests can not fetch the participant list + + + + Scenario: participants can not set publishing permissions in room for a share + # These users are only needed in very specific tests, so they are not + # created in the background step. + Given user "owner of file" exists + And user "user with access to file" exists + And user "owner of file" shares "welcome.txt" with user "user with access to file" with OCS 100 + And user "user with access to file" accepts last share + And user "owner of file" shares "welcome.txt" by link with OCS 100 + And user "guest" gets the room for last share with 200 (v1) + And user "owner of file" joins room "file last share room" with 200 (v4) + And user "user with access to file" joins room "file last share room" with 200 (v4) + And user "guest" joins room "file last share room" with 200 (v4) + And user "owner of file" loads attendees attendee ids in room "file last share room" (v4) + When user "owner of file" sets publishing permissions for "owner of file" in room "file last share room" to "NONE" with 403 (v4) + And user "owner of file" sets publishing permissions for "user with access to file" in room "file last share room" to "NONE" with 403 (v4) + And user "owner of file" sets publishing permissions for "guest" in room "file last share room" to "NONE" with 403 (v4) + And user "user with access to file" sets publishing permissions for "owner of file" in room "file last share room" to "NONE" with 403 (v4) + And user "user with access to file" sets publishing permissions for "user with access to file" in room "file last share room" to "NONE" with 403 (v4) + And user "user with access to file" sets publishing permissions for "guest" in room "file last share room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "owner of file" in room "file last share room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "user with access to file" in room "file last share room" to "NONE" with 403 (v4) + And user "guest" sets publishing permissions for "guest" in room "file last share room" to "NONE" with 403 (v4) + Then user "owner of file" sees the following attendees in room "file last share room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner of file | ALL | + | users | user with access to file | ALL | + | guests | "guest" | ALL | + And user "user with access to file" sees the following attendees in room "file last share room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner of file | ALL | + | users | user with access to file | ALL | + | guests | "guest" | ALL | + + + + # This does not make much sense, but there is no real need to block it either. + Scenario: owner can set publishing permissions in a password request room + # The user is only needed in very specific tests, so it is not created in + # the background step. + Given user "owner of file" exists + And user "owner of file" shares "welcome.txt" by link with OCS 100 + | password | 123456 | + | sendPasswordByTalk | true | + And user "guest" creates the password request room for last share with 201 (v1) + And user "guest" joins room "password request for last share room" with 200 (v4) + And user "owner of file" joins room "password request for last share room" with 200 (v4) + And user "owner of file" loads attendees attendee ids in room "password request for last share room" (v4) + When user "owner of file" sets publishing permissions for "owner of file" in room "password request for last share room" to "NONE" with 200 (v4) + And user "owner of file" sets publishing permissions for "guest" in room "password request for last share room" to "NONE" with 200 (v4) + Then user "owner of file" sees the following attendees in room "password request for last share room" with 200 (v4) + | actorType | actorId | publishingPermissions | + | users | owner of file | NONE | + | guests | "guest" | NONE | diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index 347742e3f8d..6538109286f 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -90,6 +90,7 @@ public function setUp(): void { 'geo-location-sharing', 'voice-message-sharing', 'signaling-v3', + 'publishing-permissions', ]; } diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index 47bf3157126..8064d8026b9 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -29,6 +29,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\SessionMapper; use OCA\Talk\Participant; @@ -384,7 +385,13 @@ public function testBackendRoomInvited() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $room->expects($this->once()) ->method('getParticipant') ->with($this->userId) @@ -434,7 +441,13 @@ public function testBackendRoomUserPublic() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $room->expects($this->once()) ->method('getParticipant') ->with($this->userId) @@ -484,7 +497,13 @@ public function testBackendRoomModeratorPublic() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $participant->expects($this->once()) ->method('hasModeratorPermissions') ->with(false) @@ -540,7 +559,13 @@ public function testBackendRoomAnonymousPublic() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $room->expects($this->once()) ->method('getParticipantBySession') ->with($sessionId) @@ -591,7 +616,13 @@ public function testBackendRoomInvitedPublic() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $room->expects($this->once()) ->method('getParticipantBySession') ->with($sessionId) @@ -632,6 +663,78 @@ public function testBackendRoomInvitedPublic() { ], $result->getData()); } + public function dataBackendRoomUserPublicPublishingPermissions(): array { + return [ + [Attendee::PUBLISHING_PERMISSIONS_NONE, []], + [Attendee::PUBLISHING_PERMISSIONS_AUDIO, ['publish-media']], + [Attendee::PUBLISHING_PERMISSIONS_VIDEO, ['publish-media']], + [Attendee::PUBLISHING_PERMISSIONS_VIDEO | Attendee::PUBLISHING_PERMISSIONS_VIDEO, ['publish-media']], + [Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING, ['publish-screen']], + [Attendee::PUBLISHING_PERMISSIONS_AUDIO | Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING, ['publish-media', 'publish-screen']], + [Attendee::PUBLISHING_PERMISSIONS_VIDEO | Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING, ['publish-media', 'publish-screen']], + [Attendee::PUBLISHING_PERMISSIONS_AUDIO | Attendee::PUBLISHING_PERMISSIONS_VIDEO | Attendee::PUBLISHING_PERMISSIONS_SCREENSHARING, ['publish-media', 'publish-screen']], + ]; + } + + /** + * @dataProvider dataBackendRoomUserPublicPublishingPermissions + * + * @param int $publishingPermissions + * @param array $expectedBackendPermissions + */ + public function testBackendRoomUserPublicPublishingPermissions(int $publishingPermissions, array $expectedBackendPermissions) { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $attendee = Attendee::fromRow([ + 'publishing_permissions' => $publishingPermissions, + ]); + $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); + $room->expects($this->once()) + ->method('getParticipant') + ->with($this->userId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getPropertiesForSignaling') + ->with($this->userId) + ->willReturn([ + 'name' => $roomName, + 'type' => Room::PUBLIC_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::PUBLIC_CALL, + ], + 'permissions' => $expectedBackendPermissions, + ], + ], $result->getData()); + } + public function testBackendRoomAnonymousOneToOne() { $roomToken = 'the-room'; $sessionId = 'the-session'; @@ -679,7 +782,13 @@ public function testBackendRoomSessionFromEvent() { ->with($roomToken) ->willReturn($room); + $attendee = Attendee::fromRow([ + 'publishing_permissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ]); $participant = $this->createMock(Participant::class); + $participant->expects($this->any()) + ->method('getAttendee') + ->willReturn($attendee); $room->expects($this->once()) ->method('getParticipant') ->with($this->userId) diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php index 39dabc43d44..6c3dd92f7c9 100644 --- a/tests/php/Signaling/BackendNotifierTest.php +++ b/tests/php/Signaling/BackendNotifierTest.php @@ -27,6 +27,7 @@ use OCA\Talk\Config; use OCA\Talk\Events\SignalingRoomPropertiesEvent; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\SessionMapper; use OCA\Talk\Participant; @@ -665,6 +666,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], ], @@ -674,6 +676,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], ], @@ -698,6 +701,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, ], ], 'users' => [ @@ -706,6 +710,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], [ @@ -713,6 +718,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, ], ], ], @@ -739,6 +745,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], [ @@ -746,6 +753,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, 'userId' => $notJoinedUserId, ], [ @@ -753,6 +761,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, ], ], ], @@ -771,6 +780,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], ], @@ -780,6 +790,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], [ @@ -787,6 +798,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, 'userId' => $notJoinedUserId, ], [ @@ -794,6 +806,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, ], ], ], @@ -812,6 +825,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, ], ], 'users' => [ @@ -820,6 +834,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, 'userId' => $this->userId, ], [ @@ -827,6 +842,7 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, 'userId' => $notJoinedUserId, ], [ @@ -834,6 +850,51 @@ public function testParticipantsTypeChanged() { 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + ], + ], + ], + ]); + + $this->controller->clearRequests(); + $this->participantService->updatePublishingPermissions($room, $guestParticipant, Attendee::PUBLISHING_PERMISSIONS_NONE); + + $this->assertMessageWasSent($room, [ + 'type' => 'participants', + 'participants' => [ + 'changed' => [ + [ + 'permissions' => [], + 'inCall' => 0, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, + ], + ], + 'users' => [ + [ + 'inCall' => 0, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_ALL, + 'userId' => $this->userId, + ], + [ + 'inCall' => 0, + 'lastPing' => 0, + 'sessionId' => 0, + 'participantType' => Participant::MODERATOR, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, + 'userId' => $notJoinedUserId, + ], + [ + 'inCall' => 0, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + 'publishingPermissions' => Attendee::PUBLISHING_PERMISSIONS_NONE, ], ], ],