From 37da3b7df3f641cf9f2bd5779f5ab2b69636ef7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 10 Jul 2024 08:19:54 +0200 Subject: [PATCH 01/11] refactor: Extract method to validate recording consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/Controller/CallController.php | 36 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 6e5021bb21a..909fd3f651d 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -121,17 +121,10 @@ public function getPeersForCall(): DataResponse { #[RequireParticipant] #[RequireReadWriteConversation] public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $silent = false, bool $recordingConsent = false): DataResponse { - if (!$recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { - if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_YES) { - return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); - } - if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL - && $this->room->getRecordingConsent() === RecordingService::CONSENT_REQUIRED_YES) { - return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); - } - } elseif ($recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { - $attendee = $this->participant->getAttendee(); - $this->consentService->storeConsent($this->room, $attendee->getActorType(), $attendee->getActorId()); + try { + $this->validateRecordingConsent($recordingConsent); + } catch (\InvalidArgumentException) { + return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); } $this->participantService->ensureOneToOneRoomIsFilled($this->room); @@ -158,6 +151,27 @@ public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool return new DataResponse(); } + /** + * Validates and stores recording consent. + * + * @throws \InvalidArgumentException if recording consent is required but + * not given + */ + protected function validateRecordingConsent(bool $recordingConsent): void { + if (!$recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { + if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_YES) { + throw new \InvalidArgumentException(); + } + if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL + && $this->room->getRecordingConsent() === RecordingService::CONSENT_REQUIRED_YES) { + throw new \InvalidArgumentException(); + } + } elseif ($recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { + $attendee = $this->participant->getAttendee(); + $this->consentService->storeConsent($this->room, $attendee->getActorType(), $attendee->getActorId()); + } + } + /** * Ring an attendee * From 8fa7819b28719d498c3502262f0a6c706e323093 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 25 Jul 2024 22:25:46 +0200 Subject: [PATCH 02/11] feat: Add annotation to load a federated user from the given session Signed-off-by: Joas Schilling --- .../Attribute/RequireFederatedParticipant.php | 32 ++++++++++++++ lib/Middleware/InjectionMiddleware.php | 43 +++++++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 lib/Middleware/Attribute/RequireFederatedParticipant.php diff --git a/lib/Middleware/Attribute/RequireFederatedParticipant.php b/lib/Middleware/Attribute/RequireFederatedParticipant.php new file mode 100644 index 00000000000..48fe382462a --- /dev/null +++ b/lib/Middleware/Attribute/RequireFederatedParticipant.php @@ -0,0 +1,32 @@ +sessionIdParameter; + } +} diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index 0f4b296422b..1f2e79b6e35 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -19,6 +19,7 @@ use OCA\Talk\Middleware\Attribute\AllowWithoutParticipantWhenPendingInvitation; use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireAuthenticatedParticipant; +use OCA\Talk\Middleware\Attribute\RequireFederatedParticipant; use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant; use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; @@ -34,6 +35,7 @@ use OCA\Talk\Middleware\Exceptions\ReadOnlyException; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\InvitationMapper; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\BanService; @@ -118,6 +120,12 @@ public function beforeController(Controller $controller, string $methodName): vo $this->getLoggedInOrGuest($controller, false, true); } + $attributes = $reflectionMethod->getAttributes(RequireFederatedParticipant::class); + if (!empty($attributes)) { + $sessionIdParameter = $this->readSessionIdParameterFromAttributes($attributes); + $this->getLoggedInOrGuest($controller, false, sessionIdParameter: $sessionIdParameter); + } + if (!empty($reflectionMethod->getAttributes(RequireParticipant::class))) { $this->getLoggedInOrGuest($controller, false); } @@ -153,6 +161,17 @@ public function beforeController(Controller $controller, string $methodName): vo } } + protected function readSessionIdParameterFromAttributes(array $attributes): ?string { + foreach ($attributes as $attribute) { + /** @var RequireFederatedParticipant $instance */ + $instance = $attribute->newInstance(); + if ($instance->getSessionIdParameter() !== null) { + return $instance->getSessionIdParameter(); + } + } + return null; + } + /** * @param AEnvironmentAwareController $controller * @throws ForbiddenException @@ -193,22 +212,40 @@ protected function getLoggedIn(AEnvironmentAwareController $controller, bool $mo * @throws NotAModeratorException * @throws ParticipantNotFoundException */ - protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired, bool $requireListedWhenNoParticipant = false, bool $requireFederationWhenNotLoggedIn = false): void { + protected function getLoggedInOrGuest( + AEnvironmentAwareController $controller, + bool $moderatorRequired, + bool $requireListedWhenNoParticipant = false, + bool $requireFederationWhenNotLoggedIn = false, + ?string $sessionIdParameter = null + ): void { if ($requireFederationWhenNotLoggedIn && $this->userId === null && !$this->federationAuthenticator->isFederationRequest()) { throw new ParticipantNotFoundException(); } + if ($sessionIdParameter !== null && !$this->federationAuthenticator->isFederationRequest()) { + throw new ParticipantNotFoundException(); + } + $room = $controller->getRoom(); + $sessionId = null; if (!$room instanceof Room) { $token = $this->request->getParam('token'); - $sessionId = $this->talkSession->getSessionForRoom($token); if (!$this->federationAuthenticator->isFederationRequest()) { + $sessionId = $this->talkSession->getSessionForRoom($token); $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); } else { - $room = $this->manager->getRoomByRemoteAccess($token, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $this->federationAuthenticator->getAccessToken()); + $sessionId = $sessionIdParameter !== null ? $this->request->getParam($sessionIdParameter) : null; + $room = $this->manager->getRoomByRemoteAccess($token, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $this->federationAuthenticator->getAccessToken(), $sessionId); // Get and set the participant already, so we don't retry public access $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId()); + + if ($sessionIdParameter !== null && !$participant->getSession() instanceof Session) { + // If a session is required, fail if we didn't find it + throw new ParticipantNotFoundException(); + } + $this->federationAuthenticator->authenticated($room, $participant); $controller->setParticipant($participant); } From e3f3146fbb0af8f3023f2099911c0d45d6734398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 10 Jul 2024 08:19:54 +0200 Subject: [PATCH 03/11] feat: Join and leave calls from federated conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- appinfo/routes/routesCallController.php | 4 + lib/Activity/Listener.php | 5 + lib/Controller/CallController.php | 89 +++++++++++++++ .../TalkV1/Controller/CallController.php | 106 ++++++++++++++++++ .../features/federation/call.feature | 98 ++++++++++++++++ 5 files changed, 302 insertions(+) create mode 100644 lib/Federation/Proxy/TalkV1/Controller/CallController.php create mode 100644 tests/integration/features/federation/call.feature diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php index dcb08da775f..286e40f282e 100644 --- a/appinfo/routes/routesCallController.php +++ b/appinfo/routes/routesCallController.php @@ -17,6 +17,8 @@ ['name' => 'Call#getPeersForCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::joinCall() */ ['name' => 'Call#joinCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::joinFederatedCall() */ + ['name' => 'Call#joinFederatedCall', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::ringAttendee() */ ['name' => 'Call#ringAttendee', 'url' => '/api/{apiVersion}/call/{token}/ring/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::sipDialOut() */ @@ -25,5 +27,7 @@ ['name' => 'Call#updateCallFlags', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'PUT', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::leaveCall() */ ['name' => 'Call#leaveCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'DELETE', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::leaveFederatedCall() */ + ['name' => 'Call#leaveFederatedCall', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'DELETE', 'requirements' => $requirements], ], ]; diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index 729de9752a2..2b054f7f835 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -10,6 +10,7 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Events\AParticipantModifiedEvent; +use OCA\Talk\Events\ARoomEvent; use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\BeforeCallEndedForEveryoneEvent; @@ -47,6 +48,10 @@ public function __construct( } public function handle(Event $event): void { + if ($event instanceof ARoomEvent && $event->getRoom()->isFederatedConversation()) { + return; + } + match (get_class($event)) { BeforeCallEndedForEveryoneEvent::class => $this->generateCallActivity($event->getRoom(), true, $event->getActor()), SessionLeftRoomEvent::class, diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 909fd3f651d..a41ea4fb37c 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -11,7 +11,11 @@ use OCA\Talk\Config; use OCA\Talk\Exceptions\DialOutFailedException; use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Federation\Authenticator; +use OCA\Talk\Manager; +use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireCallEnabled; +use OCA\Talk\Middleware\Attribute\RequireFederatedParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; @@ -27,6 +31,7 @@ use OCA\Talk\Service\SIPDialOutService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; @@ -41,12 +46,14 @@ class CallController extends AEnvironmentAwareController { public function __construct( string $appName, IRequest $request, + protected Manager $manager, private ConsentService $consentService, private ParticipantService $participantService, private RoomService $roomService, private IUserManager $userManager, private ITimeFactory $timeFactory, private Config $talkConfig, + protected Authenticator $federationAuthenticator, private SIPDialOutService $dialOutService, ) { parent::__construct($appName, $request); @@ -115,6 +122,7 @@ public function getPeersForCall(): DataResponse { * 400: No recording consent was given * 404: Call not found */ + #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireModeratorOrNoLobby] @@ -139,6 +147,12 @@ public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $flags = Participant::FLAG_IN_CALL | Participant::FLAG_WITH_AUDIO | Participant::FLAG_WITH_VIDEO; } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->joinFederatedCall($this->room, $this->participant, $flags, $silent, $recordingConsent); + } + if ($forcePermissions !== null && $this->participant->hasModeratorPermissions()) { $this->roomService->setPermissions($this->room, 'call', Attendee::PERMISSIONS_MODIFY_SET, $forcePermissions, true); } @@ -172,6 +186,48 @@ protected function validateRecordingConsent(bool $recordingConsent): void { } } + /** + * Join call on the host server using the session id of the federated user. + * + * @param string $sessionId Federated session id to join with + * @param int<0, 15>|null $flags In-Call flags + * @psalm-param int-mask-of|null $flags + * @param bool $silent Join the call silently + * @param bool $recordingConsent Agreement to be recorded + * @return DataResponse, array{}>|DataResponse|DataResponse + * + * 200: Call joined successfully + * 400: Conditions to join not met + * 404: Call not found + */ + #[PublicPage] + #[RequireCallEnabled] + #[RequireModeratorOrNoLobby] + #[RequireFederatedParticipant] + #[RequireReadWriteConversation] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function joinFederatedCall(string $sessionId, ?int $flags = null, bool $silent = false, bool $recordingConsent = false): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + try { + $this->validateRecordingConsent($recordingConsent); + } catch (\InvalidArgumentException) { + return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); + } + + $joined = $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); + if (!$joined) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + /** * Ring an attendee * @@ -283,6 +339,7 @@ public function updateCallFlags(int $flags): DataResponse { * 200: Call left successfully * 404: Call session not found */ + #[FederationSupported] #[PublicPage] #[RequireParticipant] public function leaveCall(bool $all = false): DataResponse { @@ -291,6 +348,12 @@ public function leaveCall(bool $all = false): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->leaveFederatedCall($this->room, $this->participant); + } + if ($all && $this->participant->hasModeratorPermissions()) { $this->participantService->endCallForEveryone($this->room, $this->participant); } else { @@ -299,4 +362,30 @@ public function leaveCall(bool $all = false): DataResponse { return new DataResponse(); } + + /** + * Leave a call on the host server using the session id of the federated + * user. + * + * @param string $sessionId Federated session id to leave with + * @return DataResponse, array{}>|DataResponse + * + * 200: Call left successfully + * 404: Call session not found + */ + #[PublicPage] + #[RequireFederatedParticipant] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function leaveFederatedCall(string $sessionId): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); + + return new DataResponse(); + } } diff --git a/lib/Federation/Proxy/TalkV1/Controller/CallController.php b/lib/Federation/Proxy/TalkV1/Controller/CallController.php new file mode 100644 index 00000000000..e0f4f0b7120 --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Controller/CallController.php @@ -0,0 +1,106 @@ + $flags In-Call flags + * @psalm-param int-mask-of $flags + * @param bool $silent Join the call silently + * @param bool $recordingConsent Agreement to be recorded + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Federated user is now in the call + * 400: Conditions to join not met + * 404: Room not found + */ + public function joinFederatedCall(Room $room, Participant $participant, int $flags, bool $silent, bool $recordingConsent): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + 'flags' => $flags, + 'silent' => $silent, + 'recordingConsent' => $recordingConsent, + ]; + + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } + + /** + * @see \OCA\Talk\Controller\RoomController::leaveFederatedCall() + * + * @param Room $room the federated room to leave the call in + * @param Participant $participant the federated user that will leave the + * call; the participant must have a session + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Federated user left the call + * 404: Room not found + */ + public function leaveFederatedCall(Room $room, Participant $participant): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + ]; + + $proxy = $this->proxy->delete( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } +} diff --git a/tests/integration/features/federation/call.feature b/tests/integration/features/federation/call.feature new file mode 100644 index 00000000000..832f9593987 --- /dev/null +++ b/tests/integration/features/federation/call.feature @@ -0,0 +1,98 @@ +Feature: federation/call + + Background: + Given using server "REMOTE" + And user "participant2" exists + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And user "participant1" exists + And the following "spreed" app config is set + | federation_enabled | yes | + + Scenario: join call + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And user "participant1" joins call "room" with 200 (v4) + | flags | 3 | + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 3 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 0 | + When user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + Then using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + | callFlag | + | 7 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 3 | + | federated_users | participant2@{$REMOTE_URL} | 7 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 7 | + + Scenario: leave call + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And user "participant1" joins call "room" with 200 (v4) + | flags | 3 | + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 7 | + When user "participant2" leaves call "LOCAL::room" with 200 (v4) + And using server "LOCAL" + And user "participant1" leaves call "room" with 200 (v4) + Then user "participant1" is participant of room "room" (v4) + | callFlag | + | 0 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 0 | + | federated_users | participant2@{$REMOTE_URL} | 0 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 0 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 0 | From 9df96cdc39c60f94c9491fbe1e4ea8fdbadb76f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 25 Jul 2024 08:00:09 +0200 Subject: [PATCH 04/11] fix: Remove unused "activeGuests" attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/Activity/Listener.php | 3 +-- lib/Manager.php | 2 -- lib/Model/SelectHelper.php | 1 - lib/Room.php | 7 +----- lib/Service/RoomService.php | 35 +++++++++------------------ tests/php/Service/RoomServiceTest.php | 1 - 6 files changed, 13 insertions(+), 36 deletions(-) diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index 2b054f7f835..ca22fe06b19 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -75,8 +75,7 @@ protected function setActive(ParticipantModifiedEvent $event): void { $this->roomService->setActiveSince( $event->getRoom(), $this->timeFactory->getDateTime(), - $participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED, - $participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS + $participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED ); } diff --git a/lib/Manager.php b/lib/Manager.php index 514536abc03..50a49fda55f 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -104,7 +104,6 @@ public function createRoomObjectFromData(array $data): Room { 'avatar' => '', 'remote_server' => '', 'remote_token' => '', - 'active_guests' => 0, 'default_permissions' => 0, 'call_permissions' => 0, 'call_flag' => 0, @@ -174,7 +173,6 @@ public function createRoomObject(array $row): Room { (string) $row['avatar'], (string) $row['remote_server'], (string) $row['remote_token'], - (int) $row['active_guests'], (int) $row['default_permissions'], (int) $row['call_permissions'], (int) $row['call_flag'], diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 3dcf3a693ea..c3022c4445f 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -26,7 +26,6 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ->addSelect($alias . 'description') ->addSelect($alias . 'password') ->addSelect($alias . 'avatar') - ->addSelect($alias . 'active_guests') ->addSelect($alias . 'active_since') ->addSelect($alias . 'default_permissions') ->addSelect($alias . 'call_permissions') diff --git a/lib/Room.php b/lib/Room.php index bba1c88c6a9..fca31047949 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -111,7 +111,6 @@ public function __construct( private string $avatar, private string $remoteServer, private string $remoteToken, - private int $activeGuests, private int $defaultPermissions, private int $callPermissions, private int $callFlag, @@ -296,7 +295,6 @@ public function setDescription(string $description): void { } public function resetActiveSince(): void { - $this->activeGuests = 0; $this->activeSince = null; } @@ -500,14 +498,11 @@ public function getParticipant(?string $userId, $sessionId = null): Participant return $this->manager->createParticipantObject($this, $row); } - public function setActiveSince(\DateTime $since, int $callFlag, bool $isGuest): void { + public function setActiveSince(\DateTime $since, int $callFlag): void { if (!$this->activeSince) { $this->activeSince = $since; } $this->callFlag |= $callFlag; - if ($isGuest) { - $this->activeGuests++; - } } public function getBreakoutRoomMode(): int { diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 2ad9a904394..7ff5d5c4200 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -834,7 +834,6 @@ public function setBreakoutRoomStatus(Room $room, int $status): bool { public function resetActiveSince(Room $room): bool { $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') - ->set('active_guests', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) ->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE)) ->set('call_flag', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) ->set('call_permissions', $update->createNamedParameter(Attendee::PERMISSIONS_DEFAULT, IQueryBuilder::PARAM_INT)) @@ -846,30 +845,18 @@ public function resetActiveSince(Room $room): bool { return (bool) $update->executeStatement(); } - public function setActiveSince(Room $room, \DateTime $since, int $callFlag, bool $isGuest): bool { - if ($isGuest && $room->getType() === Room::TYPE_PUBLIC) { - $update = $this->db->getQueryBuilder(); - $update->update('talk_rooms') - ->set('active_guests', $update->createFunction($update->getColumnName('active_guests') . ' + 1')) - ->set( - 'call_flag', - $update->expr()->bitwiseOr('call_flag', $callFlag) - ) - ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); - $update->executeStatement(); - } elseif (!$isGuest) { - $update = $this->db->getQueryBuilder(); - $update->update('talk_rooms') - ->set( - 'call_flag', - $update->expr()->bitwiseOr('call_flag', $callFlag) - ) - ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); - $update->executeStatement(); - } + public function setActiveSince(Room $room, \DateTime $since, int $callFlag): bool { + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set( + 'call_flag', + $update->expr()->bitwiseOr('call_flag', $callFlag) + ) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); if ($room->getActiveSince() instanceof \DateTime) { - $room->setActiveSince($room->getActiveSince(), $callFlag, $isGuest); + $room->setActiveSince($room->getActiveSince(), $callFlag); return false; } @@ -880,7 +867,7 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag, bool ->andWhere($update->expr()->isNull('active_since')); $update->executeStatement(); - $room->setActiveSince($since, $callFlag, $isGuest); + $room->setActiveSince($since, $callFlag); return true; } diff --git a/tests/php/Service/RoomServiceTest.php b/tests/php/Service/RoomServiceTest.php index 7307630cef9..776c0356514 100644 --- a/tests/php/Service/RoomServiceTest.php +++ b/tests/php/Service/RoomServiceTest.php @@ -350,7 +350,6 @@ public function testVerifyPassword(): void { '', '', '', - 0, Attendee::PERMISSIONS_DEFAULT, Attendee::PERMISSIONS_DEFAULT, Participant::FLAG_DISCONNECTED, From 5162e9469b9b4507b692badb601e84af724380df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 10 Jul 2024 08:38:33 +0200 Subject: [PATCH 05/11] feat: Propagate "in call" status of rooms to federated servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "In call" status is set from two different properties, "active since" and "call flag". Call flags can be modified independently, but "active since" is always linked to setting call flags, so the event needs to include both properties. Signed-off-by: Daniel Calviño Sánchez --- docs/events.md | 6 +++ lib/AppInfo/Application.php | 2 + lib/Events/AActiveSinceModifiedEvent.php | 36 ++++++++++++++++ lib/Events/ARoomModifiedEvent.php | 9 ++-- lib/Events/ActiveSinceModifiedEvent.php | 12 ++++++ lib/Events/BeforeActiveSinceModifiedEvent.php | 12 ++++++ lib/Federation/BackendNotifier.php | 43 +++++++++++++++++++ .../CloudFederationProviderTalk.php | 13 +++++- .../TalkV1/Notifier/RoomModifiedListener.php | 24 ++++++++++- lib/Service/RoomService.php | 41 +++++++++++++++++- 10 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 lib/Events/AActiveSinceModifiedEvent.php create mode 100644 lib/Events/ActiveSinceModifiedEvent.php create mode 100644 lib/Events/BeforeActiveSinceModifiedEvent.php diff --git a/docs/events.md b/docs/events.md index 93d902c6e19..89040ac1512 100644 --- a/docs/events.md +++ b/docs/events.md @@ -47,6 +47,12 @@ Allows to verify a password and set a redirect URL for the invalid case * Event: `OCA\Talk\Events\RoomPasswordVerifyEvent` * Since: 18.0.0 +### Active since modified + +* Before event: `OCA\Talk\Events\BeforeActiveSinceModifiedEvent` +* After event: `OCA\Talk\Events\ActiveSinceModifiedEvent` +* Since: 20.0.0 + ## Participant related events ### Attendees added diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d53c36e6119..444b064080a 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,6 +30,7 @@ use OCA\Talk\Config; use OCA\Talk\Dashboard\TalkWidget; use OCA\Talk\Deck\DeckPluginLoader; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\AttendeesRemovedEvent; @@ -263,6 +264,7 @@ public function register(IRegistrationContext $context): void { // Federation listeners $context->registerEventListener(BeforeRoomDeletedEvent::class, TalkV1BeforeRoomDeletedListener::class); + $context->registerEventListener(ActiveSinceModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(LobbyModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(RoomModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(ChatMessageSentEvent::class, TalkV1MessageSentListener::class); diff --git a/lib/Events/AActiveSinceModifiedEvent.php b/lib/Events/AActiveSinceModifiedEvent.php new file mode 100644 index 00000000000..29083be46b7 --- /dev/null +++ b/lib/Events/AActiveSinceModifiedEvent.php @@ -0,0 +1,36 @@ +callFlag; + } + + public function getOldCallFlag(): int { + return $this->oldCallFlag; + } +} diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index d1e92dc723a..4488e6e868c 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -12,6 +12,7 @@ use OCA\Talk\Room; abstract class ARoomModifiedEvent extends ARoomEvent { + public const PROPERTY_ACTIVE_SINCE = 'activeSince'; public const PROPERTY_AVATAR = 'avatar'; public const PROPERTY_BREAKOUT_ROOM_MODE = 'breakoutRoomMode'; public const PROPERTY_BREAKOUT_ROOM_STATUS = 'breakoutRoomStatus'; @@ -37,8 +38,8 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public function __construct( Room $room, protected string $property, - protected string|int $newValue, - protected string|int|null $oldValue = null, + protected \DateTime|string|int|null $newValue, + protected \DateTime|string|int|null $oldValue = null, protected ?Participant $actor = null, ) { parent::__construct($room); @@ -48,11 +49,11 @@ public function getProperty(): string { return $this->property; } - public function getNewValue(): string|int { + public function getNewValue(): \DateTime|string|int|null { return $this->newValue; } - public function getOldValue(): string|int|null { + public function getOldValue(): \DateTime|string|int|null { return $this->oldValue; } diff --git a/lib/Events/ActiveSinceModifiedEvent.php b/lib/Events/ActiveSinceModifiedEvent.php new file mode 100644 index 00000000000..e250899dee5 --- /dev/null +++ b/lib/Events/ActiveSinceModifiedEvent.php @@ -0,0 +1,12 @@ +sendUpdateToRemote($remote, $notification); } + /** + * Send information to remote participants that "active since" was updated + * Sent from Host server to Remote participant server + */ + public function sendRoomModifiedActiveSinceUpdate( + string $remoteServer, + int $localAttendeeId, + #[SensitiveParameter] + string $accessToken, + string $localToken, + string $changedProperty, + ?\DateTime $newValue, + ?\DateTime $oldValue, + int $callFlag, + ): ?bool { + $remote = $this->prepareRemoteUrl($remoteServer); + + if ($newValue instanceof \DateTime) { + $newValue = (string) $newValue->getTimestamp(); + } + if ($oldValue instanceof \DateTime) { + $oldValue = (string) $oldValue->getTimestamp(); + } + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + FederationManager::NOTIFICATION_ROOM_MODIFIED, + FederationManager::TALK_ROOM_RESOURCE, + (string) $localAttendeeId, + [ + 'remoteServerUrl' => $this->getServerRemoteUrl(), + 'sharedSecret' => $accessToken, + 'remoteToken' => $localToken, + 'changedProperty' => $changedProperty, + 'newValue' => $newValue, + 'oldValue' => $oldValue, + 'callFlag' => $callFlag, + ], + ); + + return $this->sendUpdateToRemote($remote, $notification); + } + /** * Send information to remote participants that the lobby was updated * Sent from Host server to Remote participant server diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 1e035510a9e..8d377932d30 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -288,7 +288,7 @@ private function shareUnshared(int $remoteAttendeeId, array $notification): arra /** * @param int $remoteAttendeeId - * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, dateTime?: string, timerReached?: bool} $notification + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, callFlag?: int, dateTime?: string, timerReached?: bool} $notification * @return array * @throws ActionNotSupportedException * @throws AuthenticationFailedException @@ -307,10 +307,19 @@ private function roomModified(int $remoteAttendeeId, array $notification): array throw new ShareNotFound(FederationManager::OCM_RESOURCE_NOT_FOUND); } - if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) { + if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE) { + if ($notification['newValue'] === null) { + $this->roomService->resetActiveSince($room); + } else { + $activeSince = \DateTime::createFromFormat('U', $notification['newValue']); + $this->roomService->setActiveSince($room, $activeSince, $notification['callFlag']); + } + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) { $this->roomService->setAvatar($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) { $this->roomService->setDescription($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_IN_CALL) { + $this->roomService->setActiveSince($room, $room->getActiveSince(), $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_LOBBY) { $dateTime = !empty($notification['dateTime']) ? \DateTime::createFromFormat('U', $notification['dateTime']) : null; $this->roomService->setLobby($room, $notification['newValue'], $dateTime, $notification['timerReached'] ?? false); diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index da0ad98f466..8141173d58e 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -8,7 +8,9 @@ namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier; +use OCA\Talk\Events\AActiveSinceModifiedEvent; use OCA\Talk\Events\AAttendeeRemovedEvent; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\ALobbyModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; use OCA\Talk\Events\LobbyModifiedEvent; @@ -34,14 +36,17 @@ public function __construct( } public function handle(Event $event): void { - if (!$event instanceof LobbyModifiedEvent + if (!$event instanceof ActiveSinceModifiedEvent + && !$event instanceof LobbyModifiedEvent && !$event instanceof RoomModifiedEvent) { return; } if (!in_array($event->getProperty(), [ + ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE, ARoomModifiedEvent::PROPERTY_AVATAR, ARoomModifiedEvent::PROPERTY_DESCRIPTION, + ARoomModifiedEvent::PROPERTY_IN_CALL, ARoomModifiedEvent::PROPERTY_LOBBY, ARoomModifiedEvent::PROPERTY_NAME, ARoomModifiedEvent::PROPERTY_READ_ONLY, @@ -54,7 +59,9 @@ public function handle(Event $event): void { foreach ($participants as $participant) { $cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId()); - if ($event instanceof ALobbyModifiedEvent) { + if ($event instanceof AActiveSinceModifiedEvent) { + $success = $this->notifyActiveSinceModified($cloudId, $participant, $event); + } elseif ($event instanceof ALobbyModifiedEvent) { $success = $this->notifyLobbyModified($cloudId, $participant, $event); } else { $success = $this->notifyRoomModified($cloudId, $participant, $event); @@ -66,6 +73,19 @@ public function handle(Event $event): void { } } + private function notifyActiveSinceModified(ICloudId $cloudId, Participant $participant, AActiveSinceModifiedEvent $event) { + return $this->backendNotifier->sendRoomModifiedActiveSinceUpdate( + $cloudId->getRemote(), + $participant->getAttendee()->getId(), + $participant->getAttendee()->getAccessToken(), + $event->getRoom()->getToken(), + $event->getProperty(), + $event->getNewValue(), + $event->getOldValue(), + $event->getCallFlag(), + ); + } + private function notifyLobbyModified(ICloudId $cloudId, Participant $participant, ALobbyModifiedEvent $event) { return $this->backendNotifier->sendRoomModifiedLobbyUpdate( $cloudId->getRemote(), diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 7ff5d5c4200..eec1459826f 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -10,7 +10,9 @@ use InvalidArgumentException; use OCA\Talk\Config; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; +use OCA\Talk\Events\BeforeActiveSinceModifiedEvent; use OCA\Talk\Events\BeforeLobbyModifiedEvent; use OCA\Talk\Events\BeforeRoomDeletedEvent; use OCA\Talk\Events\BeforeRoomModifiedEvent; @@ -832,6 +834,16 @@ public function setBreakoutRoomStatus(Room $room, int $status): bool { } public function resetActiveSince(Room $room): bool { + $oldActiveSince = $room->getActiveSince(); + $oldCallFlag = $room->getCallFlag(); + + if ($oldActiveSince === null && $oldCallFlag === Participant::FLAG_DISCONNECTED) { + return false; + } + + $event = new BeforeActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') ->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE)) @@ -842,10 +854,30 @@ public function resetActiveSince(Room $room): bool { $room->resetActiveSince(); - return (bool) $update->executeStatement(); + $result = (bool) $update->executeStatement(); + + $event = new ActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + + return $result; } public function setActiveSince(Room $room, \DateTime $since, int $callFlag): bool { + $oldActiveSince = $room->getActiveSince(); + $oldCallFlag = $room->getCallFlag(); + + if ($room->getActiveSince() instanceof \DateTime && $oldCallFlag === $callFlag) { + return false; + } + + if ($room->getActiveSince() instanceof \DateTime) { + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + } else { + $event = new BeforeActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + } + $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') ->set( @@ -857,6 +889,10 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag): boo if ($room->getActiveSince() instanceof \DateTime) { $room->setActiveSince($room->getActiveSince(), $callFlag); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + return false; } @@ -869,6 +905,9 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag): boo $room->setActiveSince($since, $callFlag); + $event = new ActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + return true; } From 6c5f9a9991434cc440e118fe8144d23e6fdce5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 25 Jul 2024 19:17:54 +0200 Subject: [PATCH 06/11] feat: Propagate "recording consent" status of rooms to federated servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In federated conversations the recording consent is got directly from the host room (through the propagated property), no matter the global configuration for recording consent in the federated instance. Signed-off-by: Daniel Calviño Sánchez --- lib/Federation/CloudFederationProviderTalk.php | 2 ++ .../Proxy/TalkV1/Notifier/RoomModifiedListener.php | 1 + lib/Service/RoomFormatter.php | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 8d377932d30..04abd2d04d3 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -327,6 +327,8 @@ private function roomModified(int $remoteAttendeeId, array $notification): array $this->roomService->setName($room, $notification['newValue'], $notification['oldValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_READ_ONLY) { $this->roomService->setReadOnly($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT) { + $this->roomService->setRecordingConsent($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_TYPE) { $this->roomService->setType($room, $notification['newValue']); } else { diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index 8141173d58e..df467a67c69 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -50,6 +50,7 @@ public function handle(Event $event): void { ARoomModifiedEvent::PROPERTY_LOBBY, ARoomModifiedEvent::PROPERTY_NAME, ARoomModifiedEvent::PROPERTY_READ_ONLY, + ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT, ARoomModifiedEvent::PROPERTY_TYPE, ], true)) { return; diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index ccad235d61b..2b0f5127a10 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -142,6 +142,10 @@ public function formatRoomV4( 'mentionPermissions' => Room::MENTION_PERMISSIONS_EVERYONE, ]; + if ($room->isFederatedConversation()) { + $roomData['recordingConsent'] = $room->getRecordingConsent(); + } + $lastActivity = $room->getLastActivity(); if ($lastActivity instanceof \DateTimeInterface) { $lastActivity = $lastActivity->getTimestamp(); @@ -221,6 +225,10 @@ public function formatRoomV4( 'mentionPermissions' => $room->getMentionPermissions(), ]); + if ($room->isFederatedConversation()) { + $roomData['recordingConsent'] = $room->getRecordingConsent(); + } + if ($currentParticipant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { if (isset($commonReadMessages[$room->getId()])) { $roomData['lastCommonReadMessage'] = $commonReadMessages[$room->getId()]; From f91bdfa38ae5922fac15bd64353715bceac8d08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 10 Jul 2024 13:29:51 +0200 Subject: [PATCH 07/11] fix: Include federated users in participants update notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The federated users are now included in the "inCall" and "participants" notfications sent from the Nextcloud server to the signaling server, so their data is also included in the "participants->update" events sent from the signaling server to the clients. Otherwise the clients would not be notified when the properties of a federated user change, not even for their own participant. Note that the "participants->update" is also received by federated clients when the default permissions of the room change, even if the default permissions themselves are not propagated to the federated room, as the change in the default permissions also cause a change in the participant permissions. Signed-off-by: Daniel Calviño Sánchez --- lib/Signaling/BackendNotifier.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index ddb4680dde8..f5f97624742 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -324,7 +324,8 @@ public function participantsModified(Room $room, array $sessionIds): void { foreach ($participants as $participant) { $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS - && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } @@ -336,7 +337,8 @@ public function participantsModified(Room $room, array $sessionIds): void { 'participantPermissions' => Attendee::PERMISSIONS_CUSTOM, 'displayName' => $attendee->getDisplayName(), ]; - if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + if ($attendee->getActorType() === Attendee::ACTOR_USERS + || $attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $data['userId'] = $attendee->getActorId(); } @@ -414,7 +416,8 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds, boo $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS - && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } @@ -426,7 +429,8 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds, boo 'participantType' => $attendee->getParticipantType(), 'participantPermissions' => $participant->getPermissions(), ]; - if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + if ($attendee->getActorType() === Attendee::ACTOR_USERS + || $attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $data['userId'] = $attendee->getActorId(); } From 1163e734e6b3a30e9a1c01dd3cd5480a17238dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 10 Jul 2024 15:45:37 +0200 Subject: [PATCH 08/11] feat: Show call button to federated users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As federated users can now join calls the setting to show the media settings or not before joining a call is also shown for federated conversations. Signed-off-by: Daniel Calviño Sánchez --- .../ConversationSettings/ConversationSettingsDialog.vue | 2 +- src/components/TopBar/CallButton.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index ef9bc0fa792..10bdf3d9517 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -183,7 +183,7 @@ export default { }, showMediaSettingsToggle() { - return (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) + return (hasTalkFeature(this.token, 'federation-v2') || !hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) }, supportBanV1() { diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue index 67ae5da335e..8f1b4f0b942 100644 --- a/src/components/TopBar/CallButton.vue +++ b/src/components/TopBar/CallButton.vue @@ -309,7 +309,7 @@ export default { return this.callEnabled && this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF && this.conversation.readOnly === CONVERSATION.STATE.READ_WRITE - && (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) + && (hasTalkFeature(this.token, 'federation-v2') || !hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) && !this.isInCall }, From 9b526a0365c3b60ddf0533b5d083332646fbb50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 16 Jul 2024 14:12:54 +0200 Subject: [PATCH 09/11] feat: Update call flags by federated participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- appinfo/routes/routesCallController.php | 2 + lib/Controller/CallController.php | 40 +++++++++++++++++ .../TalkV1/Controller/CallController.php | 37 ++++++++++++++++ .../features/federation/call.feature | 43 +++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php index 286e40f282e..fb260175f71 100644 --- a/appinfo/routes/routesCallController.php +++ b/appinfo/routes/routesCallController.php @@ -25,6 +25,8 @@ ['name' => 'Call#sipDialOut', 'url' => '/api/{apiVersion}/call/{token}/dialout/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::updateCallFlags() */ ['name' => 'Call#updateCallFlags', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'PUT', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::updateFederatedCallFlags() */ + ['name' => 'Call#updateFederatedCallFlags', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'PUT', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::leaveCall() */ ['name' => 'Call#leaveCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'DELETE', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::leaveFederatedCall() */ diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index a41ea4fb37c..d6c8a108dec 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -313,6 +313,7 @@ public function sipDialOut(int $attendeeId): DataResponse { * 400: Updating in-call flags is not possible * 404: Call session not found */ + #[FederationSupported] #[PublicPage] #[RequireParticipant] public function updateCallFlags(int $flags): DataResponse { @@ -321,6 +322,12 @@ public function updateCallFlags(int $flags): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->updateFederatedCallFlags($this->room, $this->participant, $flags); + } + try { $this->participantService->updateCallFlags($this->room, $this->participant, $flags); } catch (\Exception $exception) { @@ -330,6 +337,39 @@ public function updateCallFlags(int $flags): DataResponse { return new DataResponse(); } + /** + * Update the in-call flags on the host server using the session id of the + * federated user. + * + * @param string $sessionId Federated session id to update the flags with + * @param int<0, 15> $flags New flags + * @psalm-param int-mask-of $flags New flags + * @return DataResponse, array{}>|DataResponse + * + * 200: In-call flags updated successfully + * 400: Updating in-call flags is not possible + * 404: Call session not found + */ + #[PublicPage] + #[RequireFederatedParticipant] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function updateFederatedCallFlags(string $sessionId, int $flags): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + try { + $this->participantService->updateCallFlags($this->room, $this->participant, $flags); + } catch (\Exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + /** * Leave a call * diff --git a/lib/Federation/Proxy/TalkV1/Controller/CallController.php b/lib/Federation/Proxy/TalkV1/Controller/CallController.php index e0f4f0b7120..59b9ca98b42 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/CallController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/CallController.php @@ -71,6 +71,43 @@ public function joinFederatedCall(Room $room, Participant $participant, int $fla return new DataResponse([], $statusCode); } + /** + * @see \OCA\Talk\Controller\RoomController::updateFederatedCallFlags() + * + * @param Room $room the federated room to update the call flags in + * @param Participant $participant the federated user to update the call + * flags; the participant must have a session + * @param int<0, 15> $flags New flags + * @psalm-param int-mask-of $flags New flags + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: In-call flags updated successfully for federated user + * 400: Updating in-call flags is not possible + * 404: Room not found + */ + public function updateFederatedCallFlags(Room $room, Participant $participant, int $flags): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + 'flags' => $flags, + ]; + + $proxy = $this->proxy->put( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } + /** * @see \OCA\Talk\Controller\RoomController::leaveFederatedCall() * diff --git a/tests/integration/features/federation/call.feature b/tests/integration/features/federation/call.feature index 832f9593987..620a76c334b 100644 --- a/tests/integration/features/federation/call.feature +++ b/tests/integration/features/federation/call.feature @@ -54,6 +54,49 @@ Feature: federation/call | federated_users | participant1@{$LOCAL_URL} | 3 | | users | participant2 | 7 | + Scenario: update call flags + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 7 | + When user "participant2" updates call flags in room "LOCAL::room" to "1" with 200 (v4) + And using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + | callFlag | + | 7 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 0 | + | federated_users | participant2@{$REMOTE_URL} | 1 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 1 | + Scenario: leave call Given user "participant1" creates room "room" (v4) | roomType | 2 | From e264e82d1f2d8c61e1812d150060e9746bbbd613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 24 Jul 2024 17:58:39 +0200 Subject: [PATCH 10/11] chore: Update OpenAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- openapi-full.json | 475 ++++++++++++++++++++++++++++++ openapi.json | 475 ++++++++++++++++++++++++++++++ src/types/openapi/openapi-full.ts | 225 ++++++++++++++ src/types/openapi/openapi.ts | 225 ++++++++++++++ 4 files changed, 1400 insertions(+) diff --git a/openapi-full.json b/openapi-full.json index 1dc80a2a4e0..8fdf92ecb5a 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -4463,6 +4463,481 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + "post": { + "operationId": "call-join-federated-call", + "summary": "Join call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + }, + "flags": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "In-Call flags", + "minimum": 0, + "maximum": 15 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "Join the call silently" + }, + "recordingConsent": { + "type": "boolean", + "default": false, + "description": "Agreement to be recorded" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call joined successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Conditions to join not met", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Call not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "call-update-federated-call-flags", + "summary": "Update the in-call flags on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "flags" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to update the flags with" + }, + "flags": { + "type": "integer", + "format": "int64", + "description": "New flags", + "minimum": 0, + "maximum": 15 + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "In-call flags updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Updating in-call flags is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "call-leave-federated-call", + "summary": "Leave a call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to leave with" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call left successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { "post": { "operationId": "call-ring-attendee", diff --git a/openapi.json b/openapi.json index 368e5c05613..cfec14168c9 100644 --- a/openapi.json +++ b/openapi.json @@ -4350,6 +4350,481 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + "post": { + "operationId": "call-join-federated-call", + "summary": "Join call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + }, + "flags": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "In-Call flags", + "minimum": 0, + "maximum": 15 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "Join the call silently" + }, + "recordingConsent": { + "type": "boolean", + "default": false, + "description": "Agreement to be recorded" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call joined successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Conditions to join not met", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Call not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "call-update-federated-call-flags", + "summary": "Update the in-call flags on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "flags" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to update the flags with" + }, + "flags": { + "type": "integer", + "format": "int64", + "description": "New flags", + "minimum": 0, + "maximum": 15 + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "In-call flags updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Updating in-call flags is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "call-leave-federated-call", + "summary": "Leave a call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to leave with" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call left successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { "post": { "operationId": "call-ring-attendee", diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 4626526b895..af99d8a532b 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -261,6 +261,25 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the in-call flags on the host server using the session id of the federated user. */ + put: operations["call-update-federated-call-flags"]; + /** Join call on the host server using the session id of the federated user. */ + post: operations["call-join-federated-call"]; + /** Leave a call on the host server using the session id of the federated user. */ + delete: operations["call-leave-federated-call"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { parameters: { query?: never; @@ -3377,6 +3396,212 @@ export interface operations { }; }; }; + "call-update-federated-call-flags": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to update the flags with */ + sessionId: string; + /** + * Format: int64 + * @description New flags + */ + flags: number; + }; + }; + }; + responses: { + /** @description In-call flags updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Updating in-call flags is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-join-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + /** + * Format: int64 + * @description In-Call flags + */ + flags?: number | null; + /** + * @description Join the call silently + * @default false + */ + silent?: boolean; + /** + * @description Agreement to be recorded + * @default false + */ + recordingConsent?: boolean; + }; + }; + }; + responses: { + /** @description Call joined successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Conditions to join not met */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error?: string; + }; + }; + }; + }; + }; + /** @description Call not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-leave-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Call left successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "call-ring-attendee": { parameters: { query?: never; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 8fe9d747304..a8cbbf6891d 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -261,6 +261,25 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the in-call flags on the host server using the session id of the federated user. */ + put: operations["call-update-federated-call-flags"]; + /** Join call on the host server using the session id of the federated user. */ + post: operations["call-join-federated-call"]; + /** Leave a call on the host server using the session id of the federated user. */ + delete: operations["call-leave-federated-call"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { parameters: { query?: never; @@ -2858,6 +2877,212 @@ export interface operations { }; }; }; + "call-update-federated-call-flags": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to update the flags with */ + sessionId: string; + /** + * Format: int64 + * @description New flags + */ + flags: number; + }; + }; + }; + responses: { + /** @description In-call flags updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Updating in-call flags is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-join-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + /** + * Format: int64 + * @description In-Call flags + */ + flags?: number | null; + /** + * @description Join the call silently + * @default false + */ + silent?: boolean; + /** + * @description Agreement to be recorded + * @default false + */ + recordingConsent?: boolean; + }; + }; + }; + responses: { + /** @description Call joined successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Conditions to join not met */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error?: string; + }; + }; + }; + }; + }; + /** @description Call not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-leave-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Call left successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "call-ring-attendee": { parameters: { query?: never; From 13f092a8e823c0b16176182013f2a3b4900c342d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 25 Jul 2024 22:00:35 +0200 Subject: [PATCH 11/11] chore: Suppress Psalm issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should be properly fixed... but I have not found a way to do it :-( Signed-off-by: Daniel Calviño Sánchez --- lib/Federation/CloudFederationProviderTalk.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 04abd2d04d3..c5d205f84c4 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -328,6 +328,7 @@ private function roomModified(int $remoteAttendeeId, array $notification): array } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_READ_ONLY) { $this->roomService->setReadOnly($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT) { + /** @psalm-suppress InvalidArgument */ $this->roomService->setRecordingConsent($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_TYPE) { $this->roomService->setType($room, $notification['newValue']);