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/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/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',
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/RoomController.php b/lib/Controller/RoomController.php
index 2b9d7cefbc7..ecd2397bdfd 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;
}
@@ -1581,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,
@@ -1610,8 +1577,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 +1591,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;
}
@@ -1637,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']);
@@ -1662,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(),
@@ -1672,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;
@@ -1902,33 +1897,64 @@ 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);
- } else {
- 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, $sessionId);
+ $participant = $this->participantService->getParticipantBySession($room, $sessionId);
- try {
- $participant = $this->federationAuthenticator->getParticipant();
- } catch (ParticipantNotFoundException) {
- $participant = $this->participantService->getParticipantByActor(
- $room,
- Attendee::ACTOR_FEDERATED_USERS,
- $this->federationAuthenticator->getCloudId(),
- );
- $this->federationAuthenticator->authenticated($room, $participant);
- }
+ 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/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index 8609847e6ef..f0f9fcea46a 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;
}
@@ -175,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 = [
@@ -191,6 +206,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 +215,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/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/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/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;
+ }
+}
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/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/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php
index cda78d1d61e..4410e9affa4 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);
@@ -1619,11 +1619,25 @@ 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 getParticipantUserIdsAndFederatedUserCloudIds(Room $room, ?\DateTime $maxLastJoined = null): array {
+ return $this->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_USERS, Attendee::ACTOR_FEDERATED_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();
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/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(''),
],
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/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/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() {
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;
diff --git a/src/utils/signaling.js b/src/utils/signaling.js
index 60425aa73cc..8ce4514d9ff 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() {
@@ -1081,7 +1081,11 @@ 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('federation')
+ || !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.'),
{
@@ -1165,7 +1169,7 @@ Signaling.Standalone.prototype._joinRoomSuccess = function(token, nextcloudSessi
}
console.debug('Join room', token)
- this.doSend({
+ const message = {
type: 'room',
room: {
roomid: token,
@@ -1174,7 +1178,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))
}
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
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/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/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 | [] |
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 |
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',
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 058b3e57931..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,11 +71,13 @@ 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;
protected BanService&MockObject $banService;
protected LoggerInterface&MockObject $logger;
+ protected Authenticator&MockObject $authenticator;
protected IDBConnection $dbConnection;
protected IConfig $serverConfig;
protected ?Config $config = null;
@@ -100,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);
@@ -115,6 +119,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 +142,7 @@ private function recreateSignalingController() {
$this->clientService,
$this->banService,
$this->logger,
+ $this->authenticator,
$this->userId,
);
}
@@ -975,7 +981,7 @@ public function testLeaveRoomWithOldSession(): void {
$this->timeFactory,
$this->createMock(IHasher::class),
$this->createMock(IL10N::class),
- $this->createMock(Authenticator::class),
+ $this->authenticator,
);
$this->recreateSignalingController();
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();
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());
+ }
}