From 9617cb9655c525e08aac37601c8669b4a14bf2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 26 Jun 2024 15:47:41 +0200 Subject: [PATCH 01/17] feat: Add federation properties to signaling settings 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/SignalingController.php | 49 +++++++++++++++- .../TalkV1/Controller/SignalingController.php | 58 +++++++++++++++++++ lib/ResponseDefinitions.php | 8 +++ .../Controller/SignalingControllerTest.php | 5 +- 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 lib/Federation/Proxy/TalkV1/Controller/SignalingController.php diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 8609847e6ef..7a3ae3a2534 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -15,6 +15,7 @@ use OCA\Talk\Exceptions\ForbiddenException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Federation\Authenticator; use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; @@ -69,6 +70,7 @@ public function __construct( private IClientService $clientService, private BanService $banService, private LoggerInterface $logger, + protected Authenticator $federationAuthenticator, private ?string $userId, ) { parent::__construct($appName, $request); @@ -114,6 +116,7 @@ private function validateRecordingBackendRequest(string $data): bool { #[PublicPage] #[BruteForceProtection(action: 'talkRoomToken')] #[BruteForceProtection(action: 'talkRecordingSecret')] + #[BruteForceProtection(action: 'talkFederationAccess')] #[OpenAPI(tags: ['internal_signaling', 'external_signaling'])] public function getSettings(string $token = ''): DataResponse { $isRecordingRequest = false; @@ -128,9 +131,20 @@ public function getSettings(string $token = ''): DataResponse { $isRecordingRequest = true; } + $isTalkFederation = $this->federationAuthenticator->isFederationRequest(); + try { + $action = 'talkRoomToken'; if ($token !== '' && $isRecordingRequest) { $room = $this->manager->getRoomByToken($token); + } elseif ($token !== '' && $isTalkFederation) { + $action = 'talkFederationAccess'; + $room = $this->manager->getRoomByRemoteAccess( + $token, + Attendee::ACTOR_FEDERATED_USERS, + $this->federationAuthenticator->getCloudId(), + $this->federationAuthenticator->getAccessToken(), + ); } elseif ($token !== '') { $room = $this->manager->getRoomForUserByToken($token, $this->userId); } else { @@ -139,7 +153,7 @@ public function getSettings(string $token = ''): DataResponse { } } catch (RoomNotFoundException $e) { $response = new DataResponse([], Http::STATUS_NOT_FOUND); - $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); + $response->throttle(['token' => $token, 'action' => $action]); return $response; } @@ -191,6 +205,7 @@ public function getSettings(string $token = ''): DataResponse { 'server' => $signaling, 'ticket' => $helloAuthParams['1.0']['ticket'], 'helloAuthParams' => $helloAuthParams, + 'federation' => $this->getFederationSettings($room), 'stunservers' => $stun, 'turnservers' => $turn, 'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '', @@ -199,6 +214,38 @@ public function getSettings(string $token = ''): DataResponse { return new DataResponse($data); } + private function getFederationSettings(?Room $room): array { + if ($room === null || !$room->isFederatedConversation()) { + return []; + } + + try { + $participant = $this->participantService->getParticipant($room, $this->userId); + } catch (ParticipantNotFoundException $e) { + return []; + } + + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\SignalingController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\SignalingController::class); + $response = $proxy->getSettings($room, $participant); + + if ($response->getStatus() === Http::STATUS_NOT_FOUND) { + return []; + } + + /** @var TalkSignalingSettings $data */ + $data = $response->getData(); + + return [ + 'server' => $data['server'], + 'nextcloudServer' => $room->getRemoteServer(), + 'helloAuthParams' => [ + 'token' => $data['helloAuthParams']['2.0']['token'], + ], + 'roomId' => $room->getRemoteToken(), + ]; + } + /** * Get the welcome message from a signaling server * diff --git a/lib/Federation/Proxy/TalkV1/Controller/SignalingController.php b/lib/Federation/Proxy/TalkV1/Controller/SignalingController.php new file mode 100644 index 00000000000..4152272686d --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Controller/SignalingController.php @@ -0,0 +1,58 @@ +|DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Signaling settings returned + * 404: Room not found + */ + public function getSettings(Room $room, Participant $participant): DataResponse { + $proxy = $this->proxy->get( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v3/signaling/settings', + [ + 'token' => $room->getRemoteToken(), + ], + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + /** @var TalkSignalingSettings|array $data */ + $data = $this->proxy->getOCSData($proxy); + + return new DataResponse($data, $statusCode); + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 0d002c963da..16fa8440c6f 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -282,6 +282,14 @@ * } * * @psalm-type TalkSignalingSettings = array{ + * federation: array{ + * server: string, + * nextcloudServer: string, + * helloAuthParams: array{ + * token: string, + * }, + * roomId: string, + * }|array, * helloAuthParams: array{ * "1.0": array{ * userid: ?string, diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index 058b3e57931..69a8d104825 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -75,6 +75,7 @@ class SignalingControllerTest extends TestCase { protected IThrottler&MockObject $throttler; protected BanService&MockObject $banService; protected LoggerInterface&MockObject $logger; + protected Authenticator&MockObject $authenticator; protected IDBConnection $dbConnection; protected IConfig $serverConfig; protected ?Config $config = null; @@ -115,6 +116,7 @@ public function setUp(): void { $this->clientService = $this->createMock(IClientService::class); $this->banService = $this->createMock(BanService::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->authenticator = $this->createMock(Authenticator::class); $this->recreateSignalingController(); } @@ -137,6 +139,7 @@ private function recreateSignalingController() { $this->clientService, $this->banService, $this->logger, + $this->authenticator, $this->userId, ); } @@ -975,7 +978,7 @@ public function testLeaveRoomWithOldSession(): void { $this->timeFactory, $this->createMock(IHasher::class), $this->createMock(IL10N::class), - $this->createMock(Authenticator::class), + $this->authenticator, ); $this->recreateSignalingController(); From a857d77b185693b79daf5d69403be58402276513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 18 Jul 2024 05:39:44 +0200 Subject: [PATCH 02/17] fix: Adjust session id length in internal signaling messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The length of the "sender" and "recipient" columns in the "talk_internalsignaling" table was 255 (defined in Version11000Date20201209142525.php), but the maximum session id length is 512 (defined in Version10000Date20201015134000.php). Signed-off-by: Daniel Calviño Sánchez --- appinfo/info.xml | 2 +- .../Version20000Date20240718031959.php | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 lib/Migration/Version20000Date20240718031959.php diff --git a/appinfo/info.xml b/appinfo/info.xml index ce507e1f9ae..f66c910a10b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ * 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa. ]]> - 20.0.0-dev.7 + 20.0.0-dev.8 agpl Daniel Calviño Sánchez diff --git a/lib/Migration/Version20000Date20240718031959.php b/lib/Migration/Version20000Date20240718031959.php new file mode 100644 index 00000000000..6dd17e0a20b --- /dev/null +++ b/lib/Migration/Version20000Date20240718031959.php @@ -0,0 +1,57 @@ +hasTable('talk_internalsignaling')) { + $table = $schema->getTable('talk_internalsignaling'); + + $modified = false; + + $sender = $table->getColumn('sender'); + if ($sender->getLength() !== 512) { + $sender->setLength(512); + $modified = true; + } + + $recipient = $table->getColumn('recipient'); + if ($recipient->getLength() !== 512) { + $recipient->setLength(512); + $modified = true; + } + + if ($modified) { + return $schema; + } + } + + return null; + } +} From 6f6498a120b93d68a596ada110bcf090893e2922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 26 Jun 2024 15:49:20 +0200 Subject: [PATCH 03/17] feat: Append cloud id to Nextcloud session for federated participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will make possible to join a room in a remote Nextcloud server with the same session used in the local one avoiding the need to keep a map of sessions to convert between them and ensuring that a duplicated session id will not be used. The column length for the session id is 512, while generated session ids are only 255 characters long, so in most cases the cloud id can be added as is. Only if the cloud id is longer than 256 characters (one character needs to be reserved for the separator character) it will need to be trimmed, but that is unlikely to happen; user ids are at most 64 characters, so the "@" plus the domain would need to be longer than 192 characters. Therefore any cloud id longer than 256 characters is just trimmed at the end as a safety measure, but a fancier algorithm, for example to ellipsize it, is not needed. Signed-off-by: Daniel Calviño Sánchez --- lib/Service/SessionService.php | 23 ++++++++ tests/php/Service/SessionServiceTest.php | 75 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index bb341f14484..7acf7996289 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -94,6 +94,9 @@ public function createSessionForAttendee(Attendee $attendee, string $forceSessio } else { while (true) { $sessionId = $this->secureRandom->generate(255); + if (!empty($attendee->getInvitedCloudId())) { + $sessionId = $this->extendSessionIdWithCloudId($sessionId, $attendee->getInvitedCloudId()); + } $session->setSessionId($sessionId); try { $this->sessionMapper->insert($session); @@ -109,4 +112,24 @@ public function createSessionForAttendee(Attendee $attendee, string $forceSessio return $session; } + + /** + * Adds the given cloud id to the given session id. + * + * The session id and the cloud id are separated by '#'. + * + * If the resulting session id is longer than the column length it is + * trimmed at the end as needed. + * + * @param string $sessionId + * @param string $invitedCloudId + * @return string + */ + public function extendSessionIdWithCloudId(string $sessionId, string $invitedCloudId): string { + // Session id column length is 512, while generated session ids are 255 + // characters. + $invitedCloudId = substr($invitedCloudId, 0, 256); + + return $sessionId . '#' . $invitedCloudId; + } } diff --git a/tests/php/Service/SessionServiceTest.php b/tests/php/Service/SessionServiceTest.php index a92c768cb94..7138a356654 100644 --- a/tests/php/Service/SessionServiceTest.php +++ b/tests/php/Service/SessionServiceTest.php @@ -118,4 +118,79 @@ public function testCreateSessionForAttendeeWithoutId() { $session = $this->service->createSessionForAttendee($attendee); } + + public function testCreateSessionForAttendeeWithInvitedCloudId() { + $attendee = new Attendee(); + $attendee->setId(42); + $attendee->setActorType(Attendee::ACTOR_USERS); + $attendee->setActorId('test'); + $this->attendeeIds[] = $attendee->getId(); + + $random = self::RANDOM_254 . 'x'; + + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(255) + ->willReturn($random); + + $cloudId = 'user@server.com'; + $attendee->setInvitedCloudId($cloudId); + + $session = $this->service->createSessionForAttendee($attendee); + + self::assertEquals($random . '#' . $cloudId, $session->getSessionId()); + } + + public function testExtendSessionIdWithMaximumLengthCloudId(): void { + $attendee = new Attendee(); + $attendee->setId(42); + $attendee->setActorType(Attendee::ACTOR_USERS); + $attendee->setActorId('test'); + $this->attendeeIds[] = $attendee->getId(); + + $random = self::RANDOM_254 . 'x'; + + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(255) + ->willReturn($random); + + // User ids are 64 characters long at most; total cloud id length needs + // to leave room for the '#' joining the ids. + $cloudId = 'user123456789abcdef0123456789abcdef1123456789abcdef2123456789abc@server123456789abcdef0123456789abcdef1123456789abcdef2123456789abcdef3123456789abcdef4123456789abcdef5123456789abcdef6123456789abcdef7123456789abcdef8123456789abcdef9123456789abcdefa12345.com'; + $attendee->setInvitedCloudId($cloudId); + + $session = $this->service->createSessionForAttendee($attendee); + + self::assertEquals(256, strlen($cloudId)); + self::assertEquals(512, strlen($session->getSessionId())); + self::assertEquals($random . '#' . $cloudId, $session->getSessionId()); + } + + public function testExtendSessionIdWithTooLongCloudId(): void { + $attendee = new Attendee(); + $attendee->setId(42); + $attendee->setActorType(Attendee::ACTOR_USERS); + $attendee->setActorId('test'); + $this->attendeeIds[] = $attendee->getId(); + + $random = self::RANDOM_254 . 'x'; + + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(255) + ->willReturn($random); + + // User ids are 64 characters long at most; total cloud id length needs + // to leave room for the '#' joining the ids. + $cloudId = 'user123456789abcdef0123456789abcdef1123456789abcdef2123456789abc@server123456789abcdef0123456789abcdef1123456789abcdef2123456789abcdef3123456789abcdef4123456789abcdef5123456789abcdef6123456789abcdef7123456789abcdef8123456789abcdef9123456789abcdefa123456.com'; + $trimmedCloudId = 'user123456789abcdef0123456789abcdef1123456789abcdef2123456789abc@server123456789abcdef0123456789abcdef1123456789abcdef2123456789abcdef3123456789abcdef4123456789abcdef5123456789abcdef6123456789abcdef7123456789abcdef8123456789abcdef9123456789abcdefa123456.co'; + $attendee->setInvitedCloudId($cloudId); + + $session = $this->service->createSessionForAttendee($attendee); + + self::assertEquals(257, strlen($cloudId)); + self::assertEquals(512, strlen($session->getSessionId())); + self::assertEquals($random . '#' . $trimmedCloudId, $session->getSessionId()); + } } From 2394fa49a04c76267678cbb57b22342bf97c334e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 23 Jul 2024 18:03:08 +0200 Subject: [PATCH 04/17] test: Use real federated server in federation tests that join a room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../features/federation/chat.feature | 100 +++++++++++++----- .../features/federation/reminder.feature | 35 ++++-- 2 files changed, 105 insertions(+), 30 deletions(-) diff --git a/tests/integration/features/federation/chat.feature b/tests/integration/features/federation/chat.feature index 74bcb85eec6..7385a0e6a69 100644 --- a/tests/integration/features/federation/chat.feature +++ b/tests/integration/features/federation/chat.feature @@ -1,5 +1,9 @@ Feature: federation/chat Background: + Given using server "REMOTE" + And user "participant2" exists + And user "participant3" exists + And using server "LOCAL" Given user "participant1" exists Given user "participant2" exists Given user "participant3" exists @@ -227,12 +231,17 @@ Feature: federation/chat And user "participant2" sends message "413 Payload Too Large" to room "LOCAL::room" with 413 Scenario: Mentioning a federated user triggers a notification for them - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 2 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -251,19 +260,23 @@ Feature: federation/chat Then user "participant2" sees the following entries for dashboard widgets "spreed" (v2) | title | subtitle | link | iconUrl | sinceId | overlayIconUrl | | room | Message 1 | LOCAL::room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | | + When using server "LOCAL" When user "participant1" sends reply "Message 1-1" on message "Message 1" to room "room" with 201 + Then using server "REMOTE" Then user "participant2" sees the following entries for dashboard widgets "spreed" (v1) | title | subtitle | link | iconUrl | sinceId | overlayIconUrl | | room | You were mentioned | LOCAL::room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | | Then user "participant2" sees the following entries for dashboard widgets "spreed" (v2) | title | subtitle | link | iconUrl | sinceId | overlayIconUrl | | room | You were mentioned | LOCAL::room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | | - And user "participant1" sends message 'Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye' to room "room" with 201 + And using server "LOCAL" + And user "participant1" sends message 'Hi @"federated_user/participant2@{$REMOTE_URL}" bye' to room "room" with 201 And user "participant1" sends message 'Hi @all bye' to room "room" with 201 + Then using server "REMOTE" Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | | spreed | chat | LOCAL::room/Hi @all bye | participant1-displayname mentioned everyone in conversation room | Hi room bye | - | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye | participant1-displayname mentioned you in conversation room | Hi @participant2-displayname bye | + | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$REMOTE_URL}" bye | participant1-displayname mentioned you in conversation room | Hi @participant2-displayname bye | | spreed | chat | LOCAL::room/Message 1-1 | participant1-displayname replied to your message in conversation room | Message 1-1 | When next message request has the following parameters set | timeout | 0 | @@ -274,12 +287,17 @@ Feature: federation/chat | app | object_type | object_id | subject | message | Scenario: Mentioning a federated user as a guest also triggers a notification for them - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 3 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -292,23 +310,30 @@ Feature: federation/chat # Join and leave to clear the invite notification Given user "participant2" joins room "LOCAL::room" with 200 (v4) Given user "participant2" leaves room "LOCAL::room" with 200 (v4) + And using server "LOCAL" And user "guest" joins room "room" with 200 (v4) - When user "guest" sends message 'Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye' to room "room" with 201 + When user "guest" sends message 'Hi @"federated_user/participant2@{$REMOTE_URL}" bye' to room "room" with 201 When user "guest" sends message "Message 2" to room "room" with 201 + Then using server "REMOTE" Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | - | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye | A guest mentioned you in conversation room | Hi @participant2-displayname bye | + | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$REMOTE_URL}" bye | A guest mentioned you in conversation room | Hi @participant2-displayname bye | Then user "participant2" reads message "Message 2" in room "LOCAL::room" with 200 (v1) Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | Scenario: Mentioning a federated user with an active session does not trigger a notification but inactive does - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 3 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -321,25 +346,34 @@ Feature: federation/chat # Join and leave to clear the invite notification Given user "participant2" joins room "LOCAL::room" with 200 (v4) Given user "participant2" sets session state to 1 in room "LOCAL::room" with 200 (v4) + And using server "LOCAL" And user "guest" joins room "room" with 200 (v4) - When user "guest" sends message 'Sent to @"federated_user/participant2@{$LOCAL_REMOTE_URL}" while active' to room "room" with 201 + When user "guest" sends message 'Sent to @"federated_user/participant2@{$REMOTE_URL}" while active' to room "room" with 201 + Given using server "REMOTE" Given user "participant2" sets session state to 0 in room "LOCAL::room" with 200 (v4) - When user "guest" sends message 'User @"federated_user/participant2@{$LOCAL_REMOTE_URL}" is inactive' to room "room" with 201 + When using server "LOCAL" + When user "guest" sends message 'User @"federated_user/participant2@{$REMOTE_URL}" is inactive' to room "room" with 201 When user "guest" sends message "Message 3" to room "room" with 201 + Then using server "REMOTE" Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | - | spreed | chat | LOCAL::room/User @"federated_user/participant2@{$LOCAL_REMOTE_URL}" is inactive | A guest mentioned you in conversation room | User @participant2-displayname is inactive | + | spreed | chat | LOCAL::room/User @"federated_user/participant2@{$REMOTE_URL}" is inactive | A guest mentioned you in conversation room | User @participant2-displayname is inactive | Then user "participant2" reads message "Message 3" in room "LOCAL::room" with 200 (v1) Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | Scenario: Mentioning a federated user as a federated user that is a local user to the mentioned one also triggers a notification for them - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 3 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -349,7 +383,9 @@ Feature: federation/chat Then user "participant2" is participant of the following rooms (v4) | id | type | | LOCAL::room | 3 | - And user "participant1" adds federated_user "participant3" to room "room" with 200 (v4) + And using server "LOCAL" + And user "participant1" adds federated_user "participant3@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" And user "participant3" has the following invitations (v1) | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | @@ -362,18 +398,23 @@ Feature: federation/chat # Join and leave to clear the invite notification Given user "participant2" joins room "LOCAL::room" with 200 (v4) Given user "participant2" leaves room "LOCAL::room" with 200 (v4) - When user "participant3" sends message 'Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye' to room "LOCAL::room" with 201 + When user "participant3" sends message 'Hi @"federated_user/participant2@{$REMOTE_URL}" bye' to room "LOCAL::room" with 201 Then user "participant2" has the following notifications | app | object_type | object_id | subject | message | - | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye | participant3-displayname mentioned you in conversation room | Hi @participant2-displayname bye | + | spreed | chat | LOCAL::room/Hi @"federated_user/participant2@{$REMOTE_URL}" bye | participant3-displayname mentioned you in conversation room | Hi @participant2-displayname bye | Scenario: Mentioning and replying to self does not do notifications - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 3 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -383,7 +424,9 @@ Feature: federation/chat Then user "participant2" is participant of the following rooms (v4) | id | type | | LOCAL::room | 3 | - And user "participant1" adds federated_user "participant3" to room "room" with 200 (v4) + And using server "LOCAL" + And user "participant1" adds federated_user "participant3@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" And user "participant3" has the following invitations (v1) | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | @@ -394,6 +437,7 @@ Feature: federation/chat | id | type | | LOCAL::room | 3 | # Join and leave to clear the invite notification + Given using server "REMOTE" Given user "participant2" joins room "LOCAL::room" with 200 (v4) Given user "participant2" leaves room "LOCAL::room" with 200 (v4) When user "participant2" sends message 'Hi @"federated_user/participant2@{$LOCAL_REMOTE_URL}" bye' to room "LOCAL::room" with 201 @@ -426,12 +470,17 @@ Feature: federation/chat | app | object_type | object_id | subject | message | Scenario: Reaction on federated chat messages - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 2 | | roomName | room | - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -445,14 +494,17 @@ Feature: federation/chat Given user "participant2" joins room "LOCAL::room" with 200 (v4) Given user "participant2" leaves room "LOCAL::room" with 200 (v4) And user "participant2" sends message "Message 1" to room "LOCAL::room" with 201 + And using server "LOCAL" And user "participant1" react with "🚀" on message "Message 1" to room "room" with 201 | actorType | actorId | actorDisplayName | reaction | | users | participant1 | participant1-displayname | 🚀 | + And using server "REMOTE" And user "participant2" react with "🚀" on message "Message 1" to room "LOCAL::room" with 201 | actorType | actorId | actorDisplayName | reaction | | federated_users | participant1@{$LOCAL_URL} | participant1-displayname | 🚀 | | users | participant2 | participant2-displayname | 🚀 | + And using server "LOCAL" And user "participant1" retrieve reactions "all" of message "Message 1" in room "room" with 200 | actorType | actorId | actorDisplayName | reaction | | users | participant1 | participant1-displayname | 🚀 | - | federated_users | participant2@{$LOCAL_REMOTE_URL} | participant2-displayname | 🚀 | + | federated_users | participant2@{$REMOTE_URL} | participant2-displayname | 🚀 | diff --git a/tests/integration/features/federation/reminder.feature b/tests/integration/features/federation/reminder.feature index 415220cc2a9..1656b092040 100644 --- a/tests/integration/features/federation/reminder.feature +++ b/tests/integration/features/federation/reminder.feature @@ -1,17 +1,25 @@ Feature: federation/reminder Background: + Given using server "REMOTE" + And user "participant2" exists + And using server "LOCAL" Given user "participant1" exists Given user "participant2" exists Given user "participant3" exists Scenario: Get mention suggestions (translating local users to federated users) - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 2 | | roomName | room | And user "participant1" sends message "Message 1" to room "room" with 201 - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 | @@ -24,37 +32,52 @@ Feature: federation/reminder And user "participant2" joins room "LOCAL::room" with 200 (v4) And user "participant2" leaves room "LOCAL::room" with 200 (v4) And user "participant2" sends message "Message 2" to room "LOCAL::room" with 201 - When user "participant1" sets reminder for message "Message 2" in room "room" for time 2133349024 with 201 (v1) + When using server "LOCAL" + And user "participant1" sets reminder for message "Message 2" in room "room" for time 2133349024 with 201 (v1) + And using server "REMOTE" And user "participant2" sets reminder for message "Message 1" in room "LOCAL::room" for time 1234567 with 201 (v1) + And using server "LOCAL" And user "participant1" has the following notifications | app | object_type | object_id | subject | + And using server "REMOTE" And user "participant2" has the following notifications | app | object_type | object_id | subject | And force run "OCA\Talk\BackgroundJob\Reminder" background jobs + And using server "LOCAL" + And force run "OCA\Talk\BackgroundJob\Reminder" background jobs Then user "participant1" has the following notifications | app | object_type | object_id | subject | + And using server "REMOTE" And user "participant2" has the following notifications | app | object_type | object_id | subject | | spreed | reminder | LOCAL::room/Message 1 | Reminder: participant1-displayname in conversation room | # Participant1 sets timestamp to past so it should trigger now - When user "participant1" sets reminder for message "Message 2" in room "room" for time 1234567 with 201 (v1) + When using server "LOCAL" + And user "participant1" sets reminder for message "Message 2" in room "room" for time 1234567 with 201 (v1) And force run "OCA\Talk\BackgroundJob\Reminder" background jobs Then user "participant1" has the following notifications | app | object_type | object_id | subject | | spreed | reminder | room/Message 2 | Reminder: participant2-displayname in conversation room | + When using server "REMOTE" + And force run "OCA\Talk\BackgroundJob\Reminder" background jobs And user "participant2" deletes reminder for message "Message 1" in room "LOCAL::room" with 200 (v1) And user "participant2" has the following notifications | app | object_type | object_id | subject | Scenario: Deleting reminder before the job is executed never triggers a notification - Given the following "spreed" app config is set + Given using server "REMOTE" + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And the following "spreed" app config is set | federation_enabled | yes | Given user "participant1" creates room "room" (v4) | roomType | 2 | | roomName | room | And user "participant1" sends message "Message 1" to room "room" with 201 - And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) And user "participant1" adds user "participant3" 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 | From 668c91f61c29405250dc8877018d84c829612dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 22 Jul 2024 13:30:33 +0200 Subject: [PATCH 05/17] fix: Remove wrong comment 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/RoomController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 2b9d7cefbc7..d3fbbc3a360 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1902,7 +1902,6 @@ public function leaveRoom(string $token): DataResponse { $this->session->removeSessionForRoom($token); try { - // The participant is just joining, so enforce to not load any session if (!$this->federationAuthenticator->isFederationRequest()) { $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); $participant = $this->participantService->getParticipantBySession($room, $sessionId); From 07084e3ba7ec7774ad0e182263a11764a5051078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 22 Jul 2024 13:50:21 +0200 Subject: [PATCH 06/17] fix: Remove unused handling of federated requests in join/leaveRoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Federated request handling in "join/leaveRoom" is a leftover from the past, when federation requests were still not proxied by the federated server (and federation itself was not fully implemented). Currently federated requests are sent only to "joinFederatedRoom", so the related code in "join/leaveRoom" can be removed. Signed-off-by: Daniel Calviño Sánchez --- lib/Controller/RoomController.php | 53 +++---------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index d3fbbc3a360..7dfa315e3b9 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1506,33 +1506,16 @@ public function setPassword(string $password): DataResponse { * 409: Session already exists */ #[PublicPage] - #[BruteForceProtection(action: 'talkFederationAccess')] #[BruteForceProtection(action: 'talkRoomPassword')] #[BruteForceProtection(action: 'talkRoomToken')] public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse { $sessionId = $this->session->getSessionForRoom($token); - $isTalkFederation = $this->federationAuthenticator->isFederationRequest(); try { // The participant is just joining, so enforce to not load any session - if (!$isTalkFederation) { - $action = 'talkRoomToken'; - $room = $this->manager->getRoomForUserByToken($token, $this->userId, null); - } else { - $action = 'talkFederationAccess'; - try { - $room = $this->federationAuthenticator->getRoom(); - } catch (RoomNotFoundException) { - $room = $this->manager->getRoomByRemoteAccess( - $token, - Attendee::ACTOR_FEDERATED_USERS, - $this->federationAuthenticator->getCloudId(), - $this->federationAuthenticator->getAccessToken(), - ); - } - } + $room = $this->manager->getRoomForUserByToken($token, $this->userId, null); } catch (RoomNotFoundException $e) { $response = new DataResponse([], Http::STATUS_NOT_FOUND); - $response->throttle(['token' => $token, 'action' => $action]); + $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); return $response; } @@ -1610,8 +1593,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru if ($user instanceof IUser) { $participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']); $this->participantService->generatePinForParticipant($room, $participant); - } elseif ($isTalkFederation) { - $participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId()); } else { $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant); $this->session->setGuestActorIdForRoom($room->getToken(), $participant->getAttendee()->getActorId()); @@ -1626,7 +1607,7 @@ public function joinRoom(string $token, string $password = '', bool $force = tru return $response; } catch (UnauthorizedException $e) { $response = new DataResponse([], Http::STATUS_NOT_FOUND); - $response->throttle(['token' => $token, 'action' => $action]); + $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); return $response; } @@ -1902,32 +1883,8 @@ public function leaveRoom(string $token): DataResponse { $this->session->removeSessionForRoom($token); try { - if (!$this->federationAuthenticator->isFederationRequest()) { - $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); - $participant = $this->participantService->getParticipantBySession($room, $sessionId); - } else { - try { - $room = $this->federationAuthenticator->getRoom(); - } catch (RoomNotFoundException) { - $room = $this->manager->getRoomByRemoteAccess( - $token, - Attendee::ACTOR_FEDERATED_USERS, - $this->federationAuthenticator->getCloudId(), - $this->federationAuthenticator->getAccessToken(), - ); - } - - try { - $participant = $this->federationAuthenticator->getParticipant(); - } catch (ParticipantNotFoundException) { - $participant = $this->participantService->getParticipantByActor( - $room, - Attendee::ACTOR_FEDERATED_USERS, - $this->federationAuthenticator->getCloudId(), - ); - $this->federationAuthenticator->authenticated($room, $participant); - } - } + $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); + $participant = $this->participantService->getParticipantBySession($room, $sessionId); $this->participantService->leaveRoomAsSession($room, $participant); } catch (RoomNotFoundException|ParticipantNotFoundException) { } From 255794c3dd3d3d755636cf5ed3ee09aa0b10e270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 27 Jun 2024 14:20:06 +0200 Subject: [PATCH 07/17] feat: Join and leave federated participants using their federated session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfortunately this prevents to join a room as a local federated user, as in that case the session will be already in use. On the other hand, this avoids the need to convert sessions between host and federated servers, and with that also avoids having to keep a map between sessions. Signed-off-by: Daniel Calviño Sánchez --- appinfo/routes/routesRoomController.php | 2 + lib/Controller/RoomController.php | 114 ++++++++++++++---- .../TalkV1/Controller/RoomController.php | 45 ++++++- lib/Service/ParticipantService.php | 4 +- .../features/bootstrap/FeatureContext.php | 23 ++++ .../features/federation/join-leave.feature | 110 +++++++++++++++++ 6 files changed, 273 insertions(+), 25 deletions(-) create mode 100644 tests/integration/features/federation/join-leave.feature diff --git a/appinfo/routes/routesRoomController.php b/appinfo/routes/routesRoomController.php index 4687f1ddea3..ee176504f7b 100644 --- a/appinfo/routes/routesRoomController.php +++ b/appinfo/routes/routesRoomController.php @@ -71,6 +71,8 @@ ['name' => 'Room#resendInvitations', 'url' => '/api/{apiVersion}/room/{token}/participants/resend-invitations', 'verb' => 'POST', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::leaveRoom() */ ['name' => 'Room#leaveRoom', 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken], + /** @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom() */ + ['name' => 'Room#leaveFederatedRoom', 'url' => '/api/{apiVersion}/room/{token}/federation/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::setSessionState() */ ['name' => 'Room#setSessionState', 'url' => '/api/{apiVersion}/room/{token}/participants/state', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::promoteModerator() */ diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 7dfa315e3b9..ecd2397bdfd 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1564,22 +1564,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru $headers = []; if ($room->isFederatedConversation()) { - $participant = $this->participantService->getParticipant($room, $this->userId); - - /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */ - $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class); - $response = $proxy->joinFederatedRoom($room, $participant); - - if ($response->getStatus() === Http::STATUS_NOT_FOUND) { - $this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED); - return new DataResponse([], Http::STATUS_NOT_FOUND); - } - - $proxyHeaders = $response->getHeaders(); - if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) { - $headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash']; - } - // Skip password checking $result = [ 'result' => true, @@ -1618,23 +1602,49 @@ public function joinRoom(string $token, string $password = '', bool $force = tru $this->sessionService->updateLastPing($session, $this->timeFactory->getTime()); } + if ($room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class); + + try { + $response = $proxy->joinFederatedRoom($room, $participant); + } catch (CannotReachRemoteException $e) { + $this->participantService->leaveRoomAsSession($room, $participant); + + throw $e; + } + + if ($response->getStatus() === Http::STATUS_NOT_FOUND) { + $this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED); + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + $proxyHeaders = $response->getHeaders(); + if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) { + $headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash']; + } + } + return new DataResponse($this->formatRoom($room, $participant), Http::STATUS_OK, $headers); } /** - * Fake join a room on the host server to verify the federated user is still part of it + * Join room on the host server using the session id of the federated user. + * + * The session id can be null only for requests from Talk < 20. * * @param string $token Token of the room + * @param string $sessionId Federated session id to join with * @return DataResponse, array{X-Nextcloud-Talk-Hash: string}>|DataResponse * - * 200: Federated user is still part of the room + * 200: Federated user joined the room * 404: Room not found */ #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[PublicPage] #[BruteForceProtection(action: 'talkRoomToken')] #[BruteForceProtection(action: 'talkFederationAccess')] - public function joinFederatedRoom(string $token): DataResponse { + public function joinFederatedRoom(string $token, ?string $sessionId): DataResponse { if (!$this->federationAuthenticator->isFederationRequest()) { $response = new DataResponse(null, Http::STATUS_NOT_FOUND); $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); @@ -1643,9 +1653,9 @@ public function joinFederatedRoom(string $token): DataResponse { try { try { - $this->federationAuthenticator->getRoom(); + $room = $this->federationAuthenticator->getRoom(); } catch (RoomNotFoundException) { - $this->manager->getRoomByRemoteAccess( + $room = $this->manager->getRoomByRemoteAccess( $token, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), @@ -1653,12 +1663,16 @@ public function joinFederatedRoom(string $token): DataResponse { ); } + if ($sessionId != null) { + $participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $sessionId); + } + // Let the clients know if they need to reload capabilities $capabilities = $this->capabilities->getCapabilities(); return new DataResponse([], Http::STATUS_OK, [ 'X-Nextcloud-Talk-Hash' => sha1(json_encode($capabilities)), ]); - } catch (RoomNotFoundException) { + } catch (RoomNotFoundException|UnauthorizedException) { $response = new DataResponse(null, Http::STATUS_NOT_FOUND); $response->throttle(['token' => $token, 'action' => 'talkFederationAccess']); return $response; @@ -1885,6 +1899,62 @@ public function leaveRoom(string $token): DataResponse { try { $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); $participant = $this->participantService->getParticipantBySession($room, $sessionId); + + if ($room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class); + $response = $proxy->leaveFederatedRoom($room, $participant); + } + + $this->participantService->leaveRoomAsSession($room, $participant); + } catch (RoomNotFoundException|ParticipantNotFoundException) { + } + + return new DataResponse(); + } + + /** + * Leave room on the host server using the session id of the federated user. + * + * @param string $token Token of the room + * @param string $sessionId Federated session id to leave with + * @return DataResponse, array{}>|DataResponse + * + * 200: Successfully left the room + * 404: Room not found (non-federation request) + */ + #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] + #[PublicPage] + #[BruteForceProtection(action: 'talkRoomToken')] + public function leaveFederatedRoom(string $token, string $sessionId): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); + return $response; + } + + try { + try { + $room = $this->federationAuthenticator->getRoom(); + } catch (RoomNotFoundException) { + $room = $this->manager->getRoomByRemoteAccess( + $token, + Attendee::ACTOR_FEDERATED_USERS, + $this->federationAuthenticator->getCloudId(), + $this->federationAuthenticator->getAccessToken(), + ); + } + + try { + $participant = $this->federationAuthenticator->getParticipant(); + } catch (ParticipantNotFoundException) { + $participant = $this->participantService->getParticipantBySession( + $room, + $sessionId, + ); + $this->federationAuthenticator->authenticated($room, $participant); + } + $this->participantService->leaveRoomAsSession($room, $participant); } catch (RoomNotFoundException|ParticipantNotFoundException) { } diff --git a/lib/Federation/Proxy/TalkV1/Controller/RoomController.php b/lib/Federation/Proxy/TalkV1/Controller/RoomController.php index c384794d919..c79a2f4b6e8 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/RoomController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/RoomController.php @@ -11,6 +11,7 @@ use OCA\Talk\Exceptions\CannotReachRemoteException; use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest; use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\ResponseDefinitions; use OCA\Talk\Room; @@ -63,17 +64,25 @@ public function getParticipants(Room $room, Participant $participant): DataRespo /** * @see \OCA\Talk\Controller\RoomController::joinFederatedRoom() * + * @param Room $room the federated room to join + * @param Participant $participant the federated user to will join the room; + * the participant must have a session * @return DataResponse, array{X-Nextcloud-Talk-Proxy-Hash: string}> * @throws CannotReachRemoteException * - * 200: Federated user is still part of the room + * 200: Federated user joined the room * 404: Room not found */ public function joinFederatedRoom(Room $room, Participant $participant): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + ]; + $proxy = $this->proxy->post( $participant->getAttendee()->getInvitedCloudId(), $participant->getAttendee()->getAccessToken(), $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active', + $options, ); $statusCode = $proxy->getStatusCode(); @@ -87,6 +96,40 @@ public function joinFederatedRoom(Room $room, Participant $participant): DataRes return new DataResponse([], $statusCode, $headers); } + /** + * @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom() + * + * @param Room $room the federated room to leave + * @param Participant $participant the federated user that will leave the + * room; the participant must have a session + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Federated user left the room + */ + public function leaveFederatedRoom(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/room/' . $room->getRemoteToken() . '/federation/active', + $options, + ); + + // STATUS_NOT_FOUND is not taken into account, as it should happen only + // for non-federation requests. + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } + /** * @see \OCA\Talk\Controller\RoomController::getCapabilities() * diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index cda78d1d61e..ff6d38d1672 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -363,7 +363,7 @@ public function joinRoom(RoomService $roomService, Room $room, IUser $user, stri /** * @throws UnauthorizedException */ - public function joinRoomAsFederatedUser(Room $room, string $actorType, string $actorId): Participant { + public function joinRoomAsFederatedUser(Room $room, string $actorType, string $actorId, string $sessionId): Participant { $event = new BeforeFederatedUserJoinedRoomEvent($room, $actorId); $this->dispatcher->dispatchTyped($event); @@ -379,7 +379,7 @@ public function joinRoomAsFederatedUser(Room $room, string $actorType, string $a throw new UnauthorizedException('Participant is not allowed to join'); } - $session = $this->sessionService->createSessionForAttendee($attendee); + $session = $this->sessionService->createSessionForAttendee($attendee, $sessionId); $event = new FederatedUserJoinedRoomEvent($room, $actorId); $this->dispatcher->dispatchTyped($event); diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 1388472d640..242befb2286 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -820,6 +820,19 @@ protected function assertAttendeeList(string $identifier, ?TableNode $formData, if (isset($expectedKeys['callId'])) { $data['callId'] = (string) $attendee['callId']; } + if (isset($expectedKeys['sessionIds'])) { + $sessionIds = '['; + foreach ($attendee['sessionIds'] as $sessionId) { + if (str_contains($sessionId, '#')) { + $sessionIds .= 'SESSION' . substr($sessionId, strpos($sessionId, '#')) . ','; + } else { + $sessionIds .= 'SESSION,'; + } + } + $sessionIds .= ']'; + + $data['sessionIds'] = $sessionIds; + } if (!isset(self::$userToAttendeeId[$identifier][$attendee['actorType']])) { self::$userToAttendeeId[$identifier][$attendee['actorType']] = []; @@ -853,6 +866,16 @@ protected function assertAttendeeList(string $identifier, ?TableNode $formData, $attendee['actorId'] .= '@' . rtrim($this->localRemoteServerUrl, '/'); } + if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$LOCAL_URL}')) { + $attendee['sessionIds'] = str_replace('{$LOCAL_URL}', rtrim($this->localServerUrl, '/'), $attendee['sessionIds']); + } + if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$LOCAL_REMOTE_URL}')) { + $attendee['sessionIds'] = str_replace('{$LOCAL_REMOTE_URL}', rtrim($this->localRemoteServerUrl, '/'), $attendee['sessionIds']); + } + if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$REMOTE_URL}')) { + $attendee['sessionIds'] = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $attendee['sessionIds']); + } + if (isset($attendee['actorId'], $attendee['actorType'], $attendee['phoneNumber']) && $attendee['actorType'] === 'phones' && str_starts_with($attendee['actorId'], 'PHONE(')) { diff --git a/tests/integration/features/federation/join-leave.feature b/tests/integration/features/federation/join-leave.feature new file mode 100644 index 00000000000..fa092883d4f --- /dev/null +++ b/tests/integration/features/federation/join-leave.feature @@ -0,0 +1,110 @@ +Feature: federation/join-leave + + 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 a group room + 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 | + When 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) + Then using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | users | participant1 | 1 | [SESSION,] | + | federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] | + | users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] | + + Scenario: join a group room again without leaving it first + 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) + When user "participant2" joins room "LOCAL::room" with 200 (v4) + Then using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | users | participant1 | 1 | [SESSION,] | + | federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] | + | users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] | + + Scenario: leave a group room + 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 using server "LOCAL" + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | users | participant1 | 1 | [SESSION,] | + | federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] | + And using server "REMOTE" + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] | + | users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] | + When user "participant2" leaves room "LOCAL::room" with 200 (v4) + Then using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | users | participant1 | 1 | [SESSION,] | + | federated_users | participant2@{$REMOTE_URL} | 3 | [] | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | participantType | sessionIds | + | federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] | + | users | participant2 | 3 | [] | From 56976455e1ec32abdfcc8dde03157dba4e272a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 27 Jun 2024 14:24:05 +0200 Subject: [PATCH 08/17] feat: Send federation settings when connecting to the signaling server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With them the signaling server establishes a connection with the remote signaling server and proxies its messages, also converting the room and session ids from the remote server to the federated server as needed. Signed-off-by: Daniel Calviño Sánchez --- src/utils/signaling.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 60425aa73cc..4a5197485fe 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -977,8 +977,8 @@ Signaling.Standalone.prototype.doSend = function(msg, callback) { this.socket.send(JSON.stringify(msg)) } -Signaling.Standalone.prototype._getBackendUrl = function() { - return generateOcsUrl('apps/spreed/api/v3/signaling/backend') +Signaling.Standalone.prototype._getBackendUrl = function(baseURL = undefined) { + return generateOcsUrl('apps/spreed/api/v3/signaling/backend', {}, { baseURL }) } Signaling.Standalone.prototype.sendHello = function() { @@ -1165,7 +1165,7 @@ Signaling.Standalone.prototype._joinRoomSuccess = function(token, nextcloudSessi } console.debug('Join room', token) - this.doSend({ + const message = { type: 'room', room: { roomid: token, @@ -1174,7 +1174,18 @@ Signaling.Standalone.prototype._joinRoomSuccess = function(token, nextcloudSessi // the (Nextcloud) user is allowed to join the room. sessionid: nextcloudSessionId, }, - }, function(data) { + } + + if (this.settings.federation?.server) { + message.room.federation = { + signaling: this.settings.federation.server, + url: this._getBackendUrl(this.settings.federation.nextcloudServer), + roomid: this.settings.federation.roomId, + token: this.settings.federation.helloAuthParams.token, + } + } + + this.doSend(message, function(data) { this.joinResponseReceived(data, token) }.bind(this)) } From 1f792ebc74bfed775c51e7aea7030bc96c6b2270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 27 Jun 2024 14:28:25 +0200 Subject: [PATCH 09/17] fix: Set signaling settings again after fetching them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The signaling settings are got before joining a room, but they were simply ignored unless the conversation cluster mode was used (as in that case the signaling object was destroyed and the settings were applied to the new signaling object when chaning between different signaling servers). However, with federation the settings must be refreshed when changing between federated rooms, or between federated and non federated rooms, so for simplicity the settings are now always set after fetching them. Note that, in any case, getting the settings before joining any room might be unnecessary and it may need to be refined, but for now it is a convenient way to refresh the setting when changing between federated and non federated rooms. Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index ec66d7238a7..05bb4f9a2a8 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -125,6 +125,8 @@ async function connectSignaling(token) { }) signalingTypingHandler?.setSignaling(signaling) + } else { + signaling.setSettings(settings) } tokensInSignaling[token] = true From ad9c454ad24ecaccb188b30509cafcbecdb8b068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 22 Jul 2024 11:19:41 +0200 Subject: [PATCH 10/17] feat: Add "federation-v2" capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- docs/capabilities.md | 1 + lib/Capabilities.php | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/capabilities.md b/docs/capabilities.md index 16807130597..71adb3223a1 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -153,3 +153,4 @@ ## 20 * `ban-v1` - Whether the API to ban attendees is available * `mention-permissions` - Whether non-moderators are allowed to mention `@all` +* `federation-v2` - Whether federated session ids are used and calls are possible with federation diff --git a/lib/Capabilities.php b/lib/Capabilities.php index e7aca3c0b55..66952a5da4a 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -98,6 +98,7 @@ class Capabilities implements IPublicCapability { 'silent-send-state', 'chat-read-last', 'federation-v1', + 'federation-v2', 'ban-v1', 'chat-reference-id', 'mention-permissions', From 2875b206b02a5597ac839f41dfa18fd17d1fb69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 27 Jun 2024 14:36:43 +0200 Subject: [PATCH 11/17] feat: Show active/inactive state of federated participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before there was no session for federated participants, so it was not possible to know if a federated participant was active or not in the conversation. Now that federated participants join and leave the room with a session it is possible to show them as active or not in the room. Signed-off-by: Daniel Calviño Sánchez --- src/components/RightSidebar/Participants/Participant.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RightSidebar/Participants/Participant.vue b/src/components/RightSidebar/Participants/Participant.vue index 235933471d1..6161b46c321 100644 --- a/src/components/RightSidebar/Participants/Participant.vue +++ b/src/components/RightSidebar/Participants/Participant.vue @@ -785,7 +785,7 @@ export default { isOffline() { return !this.sessionIds.length && !this.isSearched && (this.isUserActor || this.isFederatedActor || this.isGuestActor) - && (!hasTalkFeature(this.token, 'federation-v1') || (!this.conversation.remoteServer && !this.isFederatedActor)) + && (hasTalkFeature(this.token, 'federation-v2') || !hasTalkFeature(this.token, 'federation-v1') || (!this.conversation.remoteServer && !this.isFederatedActor)) }, isGuest() { From 4f5ddbc9c24034583b548bb611040eb19fad62fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 17 Jul 2024 16:32:59 +0200 Subject: [PATCH 12/17] refactor: Format if condition in several lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/utils/signaling.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 4a5197485fe..ea5cd4977c2 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1081,7 +1081,10 @@ Signaling.Standalone.prototype.helloResponseReceived = function(data) { } } - if (!this.settings.helloAuthParams.internal && (!this.hasFeature('audio-video-permissions') || !this.hasFeature('incall-all') || !this.hasFeature('switchto'))) { + if (!this.settings.helloAuthParams.internal + && (!this.hasFeature('audio-video-permissions') + || !this.hasFeature('incall-all') + || !this.hasFeature('switchto'))) { showError( t('spreed', 'The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administration.'), { From 2e445b0490722c77462b8599335f73c54b39b22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 17 Jul 2024 16:33:28 +0200 Subject: [PATCH 13/17] feat: Set signaling server federation feature as mandatory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/Signaling/Manager.php | 1 + src/utils/signaling.js | 1 + tests/integration/mocks/FakeSignalingServer.php | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/Signaling/Manager.php b/lib/Signaling/Manager.php index a8b235d5b89..c795f7540c7 100644 --- a/lib/Signaling/Manager.php +++ b/lib/Signaling/Manager.php @@ -36,6 +36,7 @@ public function isCompatibleSignalingServer(IResponse $response): bool { $features = explode(',', $featureHeader); $features = array_map('trim', $features); return in_array('audio-video-permissions', $features, true) + && in_array('federation', $features, true) && in_array('incall-all', $features, true) && in_array('hello-v2', $features, true) && in_array('switchto', $features, true); diff --git a/src/utils/signaling.js b/src/utils/signaling.js index ea5cd4977c2..8ce4514d9ff 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1083,6 +1083,7 @@ Signaling.Standalone.prototype.helloResponseReceived = function(data) { if (!this.settings.helloAuthParams.internal && (!this.hasFeature('audio-video-permissions') + || !this.hasFeature('federation') || !this.hasFeature('incall-all') || !this.hasFeature('switchto'))) { showError( diff --git a/tests/integration/mocks/FakeSignalingServer.php b/tests/integration/mocks/FakeSignalingServer.php index 817601362ba..a69a9898d64 100644 --- a/tests/integration/mocks/FakeSignalingServer.php +++ b/tests/integration/mocks/FakeSignalingServer.php @@ -57,6 +57,7 @@ header('X-Spreed-Signaling-Features: ' . implode(',', [ 'audio-video-permissions', + 'federation', 'incall-all', 'hello-v2', 'switchto', From 8d1dfa215748c7cc85e2b50be9541ef6665d2405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sat, 20 Jul 2024 03:53:19 +0200 Subject: [PATCH 14/17] feat: Include cloud id of federated user in JWT token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cloud id will be used as the user id by the external signaling server, which will make possible to notify federated users from the remote Nextcloud server. Signed-off-by: Daniel Calviño Sánchez --- lib/Config.php | 7 +++++- lib/Controller/SignalingController.php | 3 ++- tests/php/ConfigTest.php | 22 ++++++++++++++----- .../Controller/SignalingControllerTest.php | 5 ++++- tests/php/Recording/BackendNotifierTest.php | 4 +++- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/Config.php b/lib/Config.php index 386952051cc..2b7717ef793 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -15,6 +15,7 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; use OCP\IConfig; use OCP\IGroupManager; use OCP\IURLGenerator; @@ -46,6 +47,7 @@ public function __construct( private ISecureRandom $secureRandom, private IGroupManager $groupManager, private IUserManager $userManager, + private ICloudIdManager $cloudIdManager, private IURLGenerator $urlGenerator, protected ITimeFactory $timeFactory, private IEventDispatcher $dispatcher, @@ -572,7 +574,8 @@ public function getSignalingUserData(IUser $user): array { } /** - * @param string|null $userId + * @param string|null $userId if given, the id of a user in this instance or + * a cloud id. * @return string */ private function getSignalingTicketV2(?string $userId): string { @@ -586,6 +589,8 @@ private function getSignalingTicketV2(?string $userId): string { if ($user instanceof IUser) { $data['sub'] = $user->getUID(); $data['userdata'] = $this->getSignalingUserData($user); + } elseif (!empty($userId) && $this->cloudIdManager->isValidCloudId($userId)) { + $data['sub'] = $userId; } $alg = $this->getSignalingTokenAlgorithm(); diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 7a3ae3a2534..f0f9fcea46a 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -189,13 +189,14 @@ public function getSettings(string $token = ''): DataResponse { $signalingMode = $this->talkConfig->getSignalingMode(); $signaling = $this->signalingManager->getSignalingServerLinkForConversation($room); + $helloAuthParams20UserId = $isTalkFederation ? $this->federationAuthenticator->getCloudId() : $this->userId; $helloAuthParams = [ '1.0' => [ 'userid' => $this->userId, 'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId), ], '2.0' => [ - 'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId), + 'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $helloAuthParams20UserId), ], ]; $data = [ diff --git a/tests/php/ConfigTest.php b/tests/php/ConfigTest.php index 93be5a4b3b2..a1fbc183ee6 100644 --- a/tests/php/ConfigTest.php +++ b/tests/php/ConfigTest.php @@ -15,6 +15,7 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; use OCP\IConfig; use OCP\IGroupManager; use OCP\IURLGenerator; @@ -36,12 +37,14 @@ private function createConfig(IConfig $config) { $groupManager = $this->createMock(IGroupManager::class); /** @var MockObject|IUserManager $userManager */ $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|ICloudIdManager $cloudIdManager */ + $cloudIdManager = $this->createMock(ICloudIdManager::class); /** @var MockObject|IURLGenerator $urlGenerator */ $urlGenerator = $this->createMock(IURLGenerator::class); /** @var MockObject|IEventDispatcher $dispatcher */ $dispatcher = $this->createMock(IEventDispatcher::class); - $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $cloudIdManager, $urlGenerator, $timeFactory, $dispatcher); return $helper; } @@ -145,6 +148,8 @@ public function testGenerateTurnSettings(): void { $groupManager = $this->createMock(IGroupManager::class); /** @var MockObject|IUserManager $userManager */ $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|ICloudIdManager $cloudIdManager */ + $cloudIdManager = $this->createMock(ICloudIdManager::class); /** @var MockObject|IURLGenerator $urlGenerator */ $urlGenerator = $this->createMock(IURLGenerator::class); /** @var MockObject|IEventDispatcher $dispatcher */ @@ -157,7 +162,7 @@ public function testGenerateTurnSettings(): void { ->method('generate') ->with(16) ->willReturn('abcdefghijklmnop'); - $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $cloudIdManager, $urlGenerator, $timeFactory, $dispatcher); // $settings = $helper->getTurnSettings(); @@ -221,6 +226,9 @@ public function testGenerateTurnSettingsEvent(): void { /** @var MockObject|IUserManager $userManager */ $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|ICloudIdManager $cloudIdManager */ + $cloudIdManager = $this->createMock(ICloudIdManager::class); + /** @var MockObject|IURLGenerator $urlGenerator */ $urlGenerator = $this->createMock(IURLGenerator::class); @@ -249,7 +257,7 @@ public function testGenerateTurnSettingsEvent(): void { $dispatcher->addServiceListener(BeforeTurnServersGetEvent::class, GetTurnServerListener::class); - $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $cloudIdManager, $urlGenerator, $timeFactory, $dispatcher); $settings = $helper->getTurnSettings(); $this->assertSame($servers, $settings); @@ -354,6 +362,8 @@ public function testSignalingTicketV2User(string $algo): void { $groupManager = $this->createMock(IGroupManager::class); /** @var MockObject|IUserManager $userManager */ $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|ICloudIdManager $cloudIdManager */ + $cloudIdManager = $this->createMock(ICloudIdManager::class); /** @var MockObject|IURLGenerator $urlGenerator */ $urlGenerator = $this->createMock(IURLGenerator::class); /** @var MockObject|IEventDispatcher $dispatcher */ @@ -385,7 +395,7 @@ public function testSignalingTicketV2User(string $algo): void { ->method('getDisplayName') ->willReturn('Jane Doe'); - $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $cloudIdManager, $urlGenerator, $timeFactory, $dispatcher); $config->setAppValue('spreed', 'signaling_token_alg', $algo); // Make sure new keys are generated. @@ -419,6 +429,8 @@ public function testSignalingTicketV2Anonymous(string $algo): void { $groupManager = $this->createMock(IGroupManager::class); /** @var MockObject|IUserManager $userManager */ $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|ICloudIdManager $cloudIdManager */ + $cloudIdManager = $this->createMock(ICloudIdManager::class); /** @var MockObject|IURLGenerator $urlGenerator */ $urlGenerator = $this->createMock(IURLGenerator::class); /** @var MockObject|IEventDispatcher $dispatcher */ @@ -435,7 +447,7 @@ public function testSignalingTicketV2Anonymous(string $algo): void { ->with('') ->willReturn('https://domain.invalid/nextcloud'); - $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + $helper = new Config($config, $appConfig, $secureRandom, $groupManager, $userManager, $cloudIdManager, $urlGenerator, $timeFactory, $dispatcher); $config->setAppValue('spreed', 'signaling_token_alg', $algo); // Make sure new keys are generated. diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index 69a8d104825..6f9abd877b6 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -30,6 +30,7 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; @@ -70,6 +71,7 @@ class SignalingControllerTest extends TestCase { protected SessionService&MockObject $sessionService; protected Messages&MockObject $messages; protected IUserManager&MockObject $userManager; + protected ICloudIdManager&MockObject $cloudIdManager; protected ITimeFactory&MockObject $timeFactory; protected IClientService&MockObject $clientService; protected IThrottler&MockObject $throttler; @@ -101,9 +103,10 @@ public function setUp(): void { $this->serverConfig->setAppValue('spreed', 'signaling_ticket_secret', 'the-app-ticket-secret'); $this->serverConfig->setUserValue($this->userId, 'spreed', 'signaling_ticket_secret', 'the-user-ticket-secret'); $this->userManager = $this->createMock(IUserManager::class); + $this->cloudIdManager = $this->createMock(ICloudIdManager::class); $this->dispatcher = \OCP\Server::get(IEventDispatcher::class); $urlGenerator = $this->createMock(IURLGenerator::class); - $this->config = new Config($this->serverConfig, $appConfig, $this->secureRandom, $groupManager, $this->userManager, $urlGenerator, $timeFactory, $this->dispatcher); + $this->config = new Config($this->serverConfig, $appConfig, $this->secureRandom, $groupManager, $this->userManager, $this->cloudIdManager, $urlGenerator, $timeFactory, $this->dispatcher); $this->session = $this->createMock(TalkSession::class); $this->dbConnection = \OCP\Server::get(IDBConnection::class); $this->signalingManager = $this->createMock(\OCA\Talk\Signaling\Manager::class); diff --git a/tests/php/Recording/BackendNotifierTest.php b/tests/php/Recording/BackendNotifierTest.php index 5dd320441dd..759c5a47919 100644 --- a/tests/php/Recording/BackendNotifierTest.php +++ b/tests/php/Recording/BackendNotifierTest.php @@ -23,6 +23,7 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; @@ -90,10 +91,11 @@ public function setUp(): void { $appConfig = $this->createMock(IAppConfig::class); $groupManager = $this->createMock(IGroupManager::class); $userManager = $this->createMock(IUserManager::class); + $cloudIdManager = $this->createMock(ICloudIdManager::class); $timeFactory = $this->createMock(ITimeFactory::class); $dispatcher = \OCP\Server::get(IEventDispatcher::class); - $this->config = new Config($config, $appConfig, $this->secureRandom, $groupManager, $userManager, $this->urlGenerator, $timeFactory, $dispatcher); + $this->config = new Config($config, $appConfig, $this->secureRandom, $groupManager, $userManager, $cloudIdManager, $this->urlGenerator, $timeFactory, $dispatcher); $this->recreateBackendNotifier(); From e8cff4fb7556a8811226e96bb5b7c7312f428e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 22 Jul 2024 08:53:54 +0200 Subject: [PATCH 15/17] refactor: Extract generalized getter with parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/Model/AttendeeMapper.php | 20 ++++++++++++++++++++ lib/Service/ParticipantService.php | 9 ++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 4c09cb97f5b..e7a44970848 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -101,6 +101,26 @@ public function getActorsByType(int $roomId, string $actorType, ?int $lastJoined return $this->findEntities($query); } + /** + * @param int $roomId + * @param array $actorTypes + * @param int|null $lastJoinedCall + * @return Attendee[] + */ + public function getActorsByTypes(int $roomId, array $actorTypes, ?int $lastJoinedCall = null): array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->in('actor_type', $query->createNamedParameter($actorTypes, IQueryBuilder::PARAM_STR_ARRAY))); + + if ($lastJoinedCall !== null) { + $query->andWhere($query->expr()->gte('last_joined_call', $query->createNamedParameter($lastJoinedCall, IQueryBuilder::PARAM_INT))); + } + + return $this->findEntities($query); + } + /** * @param int $roomId * @param array $participantType diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index ff6d38d1672..5a2fcfb1e92 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -1619,11 +1619,18 @@ protected function getParticipantFromQuery(IQueryBuilder $query, Room $room): Pa * @return string[] */ public function getParticipantUserIds(Room $room, ?\DateTime $maxLastJoined = null): array { + return $this->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_USERS], $maxLastJoined); + } + + /** + * @return string[] + */ + public function getParticipantActorIdsByActorType(Room $room, array $actorTypes, ?\DateTime $maxLastJoined = null): array { $maxLastJoinedTimestamp = null; if ($maxLastJoined !== null) { $maxLastJoinedTimestamp = $maxLastJoined->getTimestamp(); } - $attendees = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_USERS, $maxLastJoinedTimestamp); + $attendees = $this->attendeeMapper->getActorsByTypes($room->getId(), $actorTypes, $maxLastJoinedTimestamp); return array_map(static function (Attendee $attendee) { return $attendee->getActorId(); From 705633861094c66572678fd914239b28a1ca5b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 22 Jul 2024 09:27:15 +0200 Subject: [PATCH 16/17] fix: Notify "roomlist" updates to federated users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a participant is invited or disinvited to a conversation, or a session is removed, the affected user receives a "roomlist" message with type "invite" or "disinvite", while the rest of users in the conversation receive a "roomlist" message with type "update" and a "participant-list: refresh" property. Now both federated users and local users receive the "roomlist" update. Local users are also notified with a "roomlist" update when the properties of the room change. However, in that case the signaling server of federated users will be notified by the federated Nextcloud server when the property changes are propagated to it, so there is no need to notify federated users from the remote Nextcloud server in that case. Note, however, that independently of the users explicitly notified with the "userids" parameter (which can include inactive participants) that receive the "roomlist" message, the signaling server automatically notifies all active participants in a conversation when it is modified, so active federated users will receive a "room" message with the updated properties (which may never be reflected in the proxy conversation, as some properties are not propagated to the federated conversations). Signed-off-by: Daniel Calviño Sánchez --- lib/Service/ParticipantService.php | 7 +++++++ lib/Signaling/BackendNotifier.php | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 5a2fcfb1e92..4410e9affa4 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -1622,6 +1622,13 @@ public function getParticipantUserIds(Room $room, ?\DateTime $maxLastJoined = nu return $this->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_USERS], $maxLastJoined); } + /** + * @return string[] + */ + public function getParticipantUserIdsAndFederatedUserCloudIds(Room $room, ?\DateTime $maxLastJoined = null): array { + return $this->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_USERS, Attendee::ACTOR_FEDERATED_USERS], $maxLastJoined); + } + /** * @return string[] */ diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 38719bf76e5..ddb4680dde8 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -155,7 +155,7 @@ public function roomInvited(Room $room, array $attendees): void { 'userids' => $userIds, // TODO(fancycode): We should try to get rid of 'alluserids' and // find a better way to notify existing users to update the room. - 'alluserids' => $this->participantService->getParticipantUserIds($room), + 'alluserids' => $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room), 'properties' => $room->getPropertiesForSignaling('', false), ], ]); @@ -176,7 +176,7 @@ public function roomInvited(Room $room, array $attendees): void { * @throws \Exception */ public function roomsDisinvited(Room $room, array $attendees): void { - $allUserIds = $this->participantService->getParticipantUserIds($room); + $allUserIds = $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room); sort($allUserIds); $userIds = []; foreach ($attendees as $attendee) { @@ -212,7 +212,7 @@ public function roomsDisinvited(Room $room, array $attendees): void { * @throws \Exception */ public function roomSessionsRemoved(Room $room, array $sessionIds): void { - $allUserIds = $this->participantService->getParticipantUserIds($room); + $allUserIds = $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room); sort($allUserIds); $start = microtime(true); $this->backendRequest($room, [ @@ -245,6 +245,9 @@ public function roomModified(Room $room): void { $this->backendRequest($room, [ 'type' => 'update', 'update' => [ + // Message not sent for federated users, as they will receive + // the message from their federated Nextcloud server once the + // property change is propagated. 'userids' => $this->participantService->getParticipantUserIds($room), 'properties' => $room->getPropertiesForSignaling(''), ], From d891573ec3d6a964c09ccaa779cbf3dc3d7cea66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 23 Jul 2024 02:15:48 +0200 Subject: [PATCH 17/17] 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-federation.json | 153 ++++++++++++++++++- openapi-full.json | 193 +++++++++++++++++++++++- openapi.json | 40 +++++ src/types/openapi/openapi-federation.ts | 72 ++++++++- src/types/openapi/openapi-full.ts | 80 +++++++++- src/types/openapi/openapi.ts | 8 + 6 files changed, 534 insertions(+), 12 deletions(-) diff --git a/openapi-federation.json b/openapi-federation.json index 1cc8deaacb3..625200b62d1 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -1635,7 +1635,8 @@ "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/federation/active": { "post": { "operationId": "room-join-federated-room", - "summary": "Fake join a room on the host server to verify the federated user is still part of it", + "summary": "Join room on the host server using the session id of the federated user.", + "description": "The session id can be null only for requests from Talk < 20.", "tags": [ "room" ], @@ -1648,6 +1649,25 @@ "basic_auth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + } + } + } + } + } + }, "parameters": [ { "name": "apiVersion", @@ -1684,7 +1704,7 @@ ], "responses": { "200": { - "description": "Federated user is still part of the room", + "description": "Federated user joined the room", "headers": { "X-Nextcloud-Talk-Hash": { "schema": { @@ -1749,6 +1769,135 @@ } } } + }, + "delete": { + "operationId": "room-leave-federated-room", + "summary": "Leave room on the host server using the session id of the federated user.", + "tags": [ + "room" + ], + "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", + "description": "Token of the room", + "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": "Successfully left the room", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Room not found (non-federation request)", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } } } }, diff --git a/openapi-full.json b/openapi-full.json index 79fc01753cc..1dc80a2a4e0 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1373,6 +1373,7 @@ "SignalingSettings": { "type": "object", "required": [ + "federation", "helloAuthParams", "hideWarning", "server", @@ -1384,6 +1385,45 @@ "userId" ], "properties": { + "federation": { + "oneOf": [ + { + "type": "object", + "required": [ + "server", + "nextcloudServer", + "helloAuthParams", + "roomId" + ], + "properties": { + "server": { + "type": "string" + }, + "nextcloudServer": { + "type": "string" + }, + "helloAuthParams": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "roomId": { + "type": "string" + } + } + }, + { + "type": "array", + "maxItems": 0 + } + ] + }, "helloAuthParams": { "type": "object", "required": [ @@ -16844,7 +16884,8 @@ "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/federation/active": { "post": { "operationId": "room-join-federated-room", - "summary": "Fake join a room on the host server to verify the federated user is still part of it", + "summary": "Join room on the host server using the session id of the federated user.", + "description": "The session id can be null only for requests from Talk < 20.", "tags": [ "room" ], @@ -16857,6 +16898,25 @@ "basic_auth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + } + } + } + } + } + }, "parameters": [ { "name": "apiVersion", @@ -16893,7 +16953,7 @@ ], "responses": { "200": { - "description": "Federated user is still part of the room", + "description": "Federated user joined the room", "headers": { "X-Nextcloud-Talk-Hash": { "schema": { @@ -16958,6 +17018,135 @@ } } } + }, + "delete": { + "operationId": "room-leave-federated-room", + "summary": "Leave room on the host server using the session id of the federated user.", + "tags": [ + "room" + ], + "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", + "description": "Token of the room", + "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": "Successfully left the room", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Room not found (non-federation request)", + "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}/bot/{token}/message": { diff --git a/openapi.json b/openapi.json index 753c6950833..368e5c05613 100644 --- a/openapi.json +++ b/openapi.json @@ -1260,6 +1260,7 @@ "SignalingSettings": { "type": "object", "required": [ + "federation", "helloAuthParams", "hideWarning", "server", @@ -1271,6 +1272,45 @@ "userId" ], "properties": { + "federation": { + "oneOf": [ + { + "type": "object", + "required": [ + "server", + "nextcloudServer", + "helloAuthParams", + "roomId" + ], + "properties": { + "server": { + "type": "string" + }, + "nextcloudServer": { + "type": "string" + }, + "helloAuthParams": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "roomId": { + "type": "string" + } + } + }, + { + "type": "array", + "maxItems": 0 + } + ] + }, "helloAuthParams": { "type": "object", "required": [ diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 35b06381e27..394ad27a625 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -125,9 +125,13 @@ export type paths = { }; get?: never; put?: never; - /** Fake join a room on the host server to verify the federated user is still part of it */ + /** + * Join room on the host server using the session id of the federated user. + * @description The session id can be null only for requests from Talk < 20. + */ post: operations["room-join-federated-room"]; - delete?: never; + /** Leave room on the host server using the session id of the federated user. */ + delete: operations["room-leave-federated-room"]; options?: never; head?: never; patch?: never; @@ -719,9 +723,16 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + }; + }; + }; responses: { - /** @description Federated user is still part of the room */ + /** @description Federated user joined the room */ 200: { headers: { "X-Nextcloud-Talk-Hash"?: string; @@ -752,4 +763,57 @@ export interface operations { }; }; }; + "room-leave-federated-room": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + /** @description Token of the room */ + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Successfully left the room */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Room not found (non-federation request) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; } diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 17b8f784897..4626526b895 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1391,9 +1391,13 @@ export type paths = { }; get?: never; put?: never; - /** Fake join a room on the host server to verify the federated user is still part of it */ + /** + * Join room on the host server using the session id of the federated user. + * @description The session id can be null only for requests from Talk < 20. + */ post: operations["room-join-federated-room"]; - delete?: never; + /** Leave room on the host server using the session id of the federated user. */ + delete: operations["room-leave-federated-room"]; options?: never; head?: never; patch?: never; @@ -2155,6 +2159,14 @@ export type components = { userId: string; }; SignalingSettings: { + federation: { + server: string; + nextcloudServer: string; + helloAuthParams: { + token: string; + }; + roomId: string; + } | unknown[]; helloAuthParams: { "1.0": { userid: string | null; @@ -8583,9 +8595,16 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + }; + }; + }; responses: { - /** @description Federated user is still part of the room */ + /** @description Federated user joined the room */ 200: { headers: { "X-Nextcloud-Talk-Hash"?: string; @@ -8616,6 +8635,59 @@ export interface operations { }; }; }; + "room-leave-federated-room": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + /** @description Token of the room */ + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Successfully left the room */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Room not found (non-federation request) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "bot-send-message": { parameters: { query?: never; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 470b4645bac..8fe9d747304 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1640,6 +1640,14 @@ export type components = { userId: string; }; SignalingSettings: { + federation: { + server: string; + nextcloudServer: string; + helloAuthParams: { + token: string; + }; + roomId: string; + } | unknown[]; helloAuthParams: { "1.0": { userid: string | null;