diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php index dcb08da775f..fb260175f71 100644 --- a/appinfo/routes/routesCallController.php +++ b/appinfo/routes/routesCallController.php @@ -17,13 +17,19 @@ ['name' => 'Call#getPeersForCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::joinCall() */ ['name' => 'Call#joinCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::joinFederatedCall() */ + ['name' => 'Call#joinFederatedCall', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::ringAttendee() */ ['name' => 'Call#ringAttendee', 'url' => '/api/{apiVersion}/call/{token}/ring/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::sipDialOut() */ ['name' => 'Call#sipDialOut', 'url' => '/api/{apiVersion}/call/{token}/dialout/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::updateCallFlags() */ ['name' => 'Call#updateCallFlags', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'PUT', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::updateFederatedCallFlags() */ + ['name' => 'Call#updateFederatedCallFlags', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'PUT', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::leaveCall() */ ['name' => 'Call#leaveCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'DELETE', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::leaveFederatedCall() */ + ['name' => 'Call#leaveFederatedCall', 'url' => '/api/{apiVersion}/call/{token}/federation', 'verb' => 'DELETE', 'requirements' => $requirements], ], ]; diff --git a/docs/events.md b/docs/events.md index 93d902c6e19..89040ac1512 100644 --- a/docs/events.md +++ b/docs/events.md @@ -47,6 +47,12 @@ Allows to verify a password and set a redirect URL for the invalid case * Event: `OCA\Talk\Events\RoomPasswordVerifyEvent` * Since: 18.0.0 +### Active since modified + +* Before event: `OCA\Talk\Events\BeforeActiveSinceModifiedEvent` +* After event: `OCA\Talk\Events\ActiveSinceModifiedEvent` +* Since: 20.0.0 + ## Participant related events ### Attendees added diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index 729de9752a2..ca22fe06b19 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -10,6 +10,7 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Events\AParticipantModifiedEvent; +use OCA\Talk\Events\ARoomEvent; use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\BeforeCallEndedForEveryoneEvent; @@ -47,6 +48,10 @@ public function __construct( } public function handle(Event $event): void { + if ($event instanceof ARoomEvent && $event->getRoom()->isFederatedConversation()) { + return; + } + match (get_class($event)) { BeforeCallEndedForEveryoneEvent::class => $this->generateCallActivity($event->getRoom(), true, $event->getActor()), SessionLeftRoomEvent::class, @@ -70,8 +75,7 @@ protected function setActive(ParticipantModifiedEvent $event): void { $this->roomService->setActiveSince( $event->getRoom(), $this->timeFactory->getDateTime(), - $participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED, - $participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS + $participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED ); } diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d53c36e6119..444b064080a 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,6 +30,7 @@ use OCA\Talk\Config; use OCA\Talk\Dashboard\TalkWidget; use OCA\Talk\Deck\DeckPluginLoader; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\AttendeesRemovedEvent; @@ -263,6 +264,7 @@ public function register(IRegistrationContext $context): void { // Federation listeners $context->registerEventListener(BeforeRoomDeletedEvent::class, TalkV1BeforeRoomDeletedListener::class); + $context->registerEventListener(ActiveSinceModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(LobbyModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(RoomModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(ChatMessageSentEvent::class, TalkV1MessageSentListener::class); diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 6e5021bb21a..d6c8a108dec 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -11,7 +11,11 @@ use OCA\Talk\Config; use OCA\Talk\Exceptions\DialOutFailedException; use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Federation\Authenticator; +use OCA\Talk\Manager; +use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireCallEnabled; +use OCA\Talk\Middleware\Attribute\RequireFederatedParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; @@ -27,6 +31,7 @@ use OCA\Talk\Service\SIPDialOutService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; @@ -41,12 +46,14 @@ class CallController extends AEnvironmentAwareController { public function __construct( string $appName, IRequest $request, + protected Manager $manager, private ConsentService $consentService, private ParticipantService $participantService, private RoomService $roomService, private IUserManager $userManager, private ITimeFactory $timeFactory, private Config $talkConfig, + protected Authenticator $federationAuthenticator, private SIPDialOutService $dialOutService, ) { parent::__construct($appName, $request); @@ -115,23 +122,17 @@ public function getPeersForCall(): DataResponse { * 400: No recording consent was given * 404: Call not found */ + #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequireReadWriteConversation] public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $silent = false, bool $recordingConsent = false): DataResponse { - if (!$recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { - if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_YES) { - return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); - } - if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL - && $this->room->getRecordingConsent() === RecordingService::CONSENT_REQUIRED_YES) { - return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); - } - } elseif ($recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { - $attendee = $this->participant->getAttendee(); - $this->consentService->storeConsent($this->room, $attendee->getActorType(), $attendee->getActorId()); + try { + $this->validateRecordingConsent($recordingConsent); + } catch (\InvalidArgumentException) { + return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); } $this->participantService->ensureOneToOneRoomIsFilled($this->room); @@ -146,6 +147,12 @@ public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $flags = Participant::FLAG_IN_CALL | Participant::FLAG_WITH_AUDIO | Participant::FLAG_WITH_VIDEO; } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->joinFederatedCall($this->room, $this->participant, $flags, $silent, $recordingConsent); + } + if ($forcePermissions !== null && $this->participant->hasModeratorPermissions()) { $this->roomService->setPermissions($this->room, 'call', Attendee::PERMISSIONS_MODIFY_SET, $forcePermissions, true); } @@ -158,6 +165,69 @@ public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool return new DataResponse(); } + /** + * Validates and stores recording consent. + * + * @throws \InvalidArgumentException if recording consent is required but + * not given + */ + protected function validateRecordingConsent(bool $recordingConsent): void { + if (!$recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { + if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_YES) { + throw new \InvalidArgumentException(); + } + if ($this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL + && $this->room->getRecordingConsent() === RecordingService::CONSENT_REQUIRED_YES) { + throw new \InvalidArgumentException(); + } + } elseif ($recordingConsent && $this->talkConfig->recordingConsentRequired() !== RecordingService::CONSENT_REQUIRED_NO) { + $attendee = $this->participant->getAttendee(); + $this->consentService->storeConsent($this->room, $attendee->getActorType(), $attendee->getActorId()); + } + } + + /** + * Join call on the host server using the session id of the federated user. + * + * @param string $sessionId Federated session id to join with + * @param int<0, 15>|null $flags In-Call flags + * @psalm-param int-mask-of|null $flags + * @param bool $silent Join the call silently + * @param bool $recordingConsent Agreement to be recorded + * @return DataResponse, array{}>|DataResponse|DataResponse + * + * 200: Call joined successfully + * 400: Conditions to join not met + * 404: Call not found + */ + #[PublicPage] + #[RequireCallEnabled] + #[RequireModeratorOrNoLobby] + #[RequireFederatedParticipant] + #[RequireReadWriteConversation] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function joinFederatedCall(string $sessionId, ?int $flags = null, bool $silent = false, bool $recordingConsent = false): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + try { + $this->validateRecordingConsent($recordingConsent); + } catch (\InvalidArgumentException) { + return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); + } + + $joined = $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); + if (!$joined) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + /** * Ring an attendee * @@ -243,6 +313,7 @@ public function sipDialOut(int $attendeeId): DataResponse { * 400: Updating in-call flags is not possible * 404: Call session not found */ + #[FederationSupported] #[PublicPage] #[RequireParticipant] public function updateCallFlags(int $flags): DataResponse { @@ -251,6 +322,12 @@ public function updateCallFlags(int $flags): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->updateFederatedCallFlags($this->room, $this->participant, $flags); + } + try { $this->participantService->updateCallFlags($this->room, $this->participant, $flags); } catch (\Exception $exception) { @@ -260,6 +337,39 @@ public function updateCallFlags(int $flags): DataResponse { return new DataResponse(); } + /** + * Update the in-call flags on the host server using the session id of the + * federated user. + * + * @param string $sessionId Federated session id to update the flags with + * @param int<0, 15> $flags New flags + * @psalm-param int-mask-of $flags New flags + * @return DataResponse, array{}>|DataResponse + * + * 200: In-call flags updated successfully + * 400: Updating in-call flags is not possible + * 404: Call session not found + */ + #[PublicPage] + #[RequireFederatedParticipant] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function updateFederatedCallFlags(string $sessionId, int $flags): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + try { + $this->participantService->updateCallFlags($this->room, $this->participant, $flags); + } catch (\Exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + /** * Leave a call * @@ -269,6 +379,7 @@ public function updateCallFlags(int $flags): DataResponse { * 200: Call left successfully * 404: Call session not found */ + #[FederationSupported] #[PublicPage] #[RequireParticipant] public function leaveCall(bool $all = false): DataResponse { @@ -277,6 +388,12 @@ public function leaveCall(bool $all = false): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->leaveFederatedCall($this->room, $this->participant); + } + if ($all && $this->participant->hasModeratorPermissions()) { $this->participantService->endCallForEveryone($this->room, $this->participant); } else { @@ -285,4 +402,30 @@ public function leaveCall(bool $all = false): DataResponse { return new DataResponse(); } + + /** + * Leave a call on the host server using the session id of the federated + * user. + * + * @param string $sessionId Federated session id to leave with + * @return DataResponse, array{}>|DataResponse + * + * 200: Call left successfully + * 404: Call session not found + */ + #[PublicPage] + #[RequireFederatedParticipant] + #[BruteForceProtection(action: 'talkFederationAccess')] + #[BruteForceProtection(action: 'talkRoomToken')] + public function leaveFederatedCall(string $sessionId): DataResponse { + if (!$this->federationAuthenticator->isFederationRequest()) { + $response = new DataResponse(null, Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $this->room->getToken(), 'action' => 'talkRoomToken']); + return $response; + } + + $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); + + return new DataResponse(); + } } diff --git a/lib/Events/AActiveSinceModifiedEvent.php b/lib/Events/AActiveSinceModifiedEvent.php new file mode 100644 index 00000000000..29083be46b7 --- /dev/null +++ b/lib/Events/AActiveSinceModifiedEvent.php @@ -0,0 +1,36 @@ +callFlag; + } + + public function getOldCallFlag(): int { + return $this->oldCallFlag; + } +} diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index d1e92dc723a..4488e6e868c 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -12,6 +12,7 @@ use OCA\Talk\Room; abstract class ARoomModifiedEvent extends ARoomEvent { + public const PROPERTY_ACTIVE_SINCE = 'activeSince'; public const PROPERTY_AVATAR = 'avatar'; public const PROPERTY_BREAKOUT_ROOM_MODE = 'breakoutRoomMode'; public const PROPERTY_BREAKOUT_ROOM_STATUS = 'breakoutRoomStatus'; @@ -37,8 +38,8 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public function __construct( Room $room, protected string $property, - protected string|int $newValue, - protected string|int|null $oldValue = null, + protected \DateTime|string|int|null $newValue, + protected \DateTime|string|int|null $oldValue = null, protected ?Participant $actor = null, ) { parent::__construct($room); @@ -48,11 +49,11 @@ public function getProperty(): string { return $this->property; } - public function getNewValue(): string|int { + public function getNewValue(): \DateTime|string|int|null { return $this->newValue; } - public function getOldValue(): string|int|null { + public function getOldValue(): \DateTime|string|int|null { return $this->oldValue; } diff --git a/lib/Events/ActiveSinceModifiedEvent.php b/lib/Events/ActiveSinceModifiedEvent.php new file mode 100644 index 00000000000..e250899dee5 --- /dev/null +++ b/lib/Events/ActiveSinceModifiedEvent.php @@ -0,0 +1,12 @@ +sendUpdateToRemote($remote, $notification); } + /** + * Send information to remote participants that "active since" was updated + * Sent from Host server to Remote participant server + */ + public function sendRoomModifiedActiveSinceUpdate( + string $remoteServer, + int $localAttendeeId, + #[SensitiveParameter] + string $accessToken, + string $localToken, + string $changedProperty, + ?\DateTime $newValue, + ?\DateTime $oldValue, + int $callFlag, + ): ?bool { + $remote = $this->prepareRemoteUrl($remoteServer); + + if ($newValue instanceof \DateTime) { + $newValue = (string) $newValue->getTimestamp(); + } + if ($oldValue instanceof \DateTime) { + $oldValue = (string) $oldValue->getTimestamp(); + } + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + FederationManager::NOTIFICATION_ROOM_MODIFIED, + FederationManager::TALK_ROOM_RESOURCE, + (string) $localAttendeeId, + [ + 'remoteServerUrl' => $this->getServerRemoteUrl(), + 'sharedSecret' => $accessToken, + 'remoteToken' => $localToken, + 'changedProperty' => $changedProperty, + 'newValue' => $newValue, + 'oldValue' => $oldValue, + 'callFlag' => $callFlag, + ], + ); + + return $this->sendUpdateToRemote($remote, $notification); + } + /** * Send information to remote participants that the lobby was updated * Sent from Host server to Remote participant server diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 1e035510a9e..c5d205f84c4 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -288,7 +288,7 @@ private function shareUnshared(int $remoteAttendeeId, array $notification): arra /** * @param int $remoteAttendeeId - * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, dateTime?: string, timerReached?: bool} $notification + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, callFlag?: int, dateTime?: string, timerReached?: bool} $notification * @return array * @throws ActionNotSupportedException * @throws AuthenticationFailedException @@ -307,10 +307,19 @@ private function roomModified(int $remoteAttendeeId, array $notification): array throw new ShareNotFound(FederationManager::OCM_RESOURCE_NOT_FOUND); } - if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) { + if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE) { + if ($notification['newValue'] === null) { + $this->roomService->resetActiveSince($room); + } else { + $activeSince = \DateTime::createFromFormat('U', $notification['newValue']); + $this->roomService->setActiveSince($room, $activeSince, $notification['callFlag']); + } + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) { $this->roomService->setAvatar($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) { $this->roomService->setDescription($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_IN_CALL) { + $this->roomService->setActiveSince($room, $room->getActiveSince(), $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_LOBBY) { $dateTime = !empty($notification['dateTime']) ? \DateTime::createFromFormat('U', $notification['dateTime']) : null; $this->roomService->setLobby($room, $notification['newValue'], $dateTime, $notification['timerReached'] ?? false); @@ -318,6 +327,9 @@ private function roomModified(int $remoteAttendeeId, array $notification): array $this->roomService->setName($room, $notification['newValue'], $notification['oldValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_READ_ONLY) { $this->roomService->setReadOnly($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT) { + /** @psalm-suppress InvalidArgument */ + $this->roomService->setRecordingConsent($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_TYPE) { $this->roomService->setType($room, $notification['newValue']); } else { diff --git a/lib/Federation/Proxy/TalkV1/Controller/CallController.php b/lib/Federation/Proxy/TalkV1/Controller/CallController.php new file mode 100644 index 00000000000..59b9ca98b42 --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Controller/CallController.php @@ -0,0 +1,143 @@ + $flags In-Call flags + * @psalm-param int-mask-of $flags + * @param bool $silent Join the call silently + * @param bool $recordingConsent Agreement to be recorded + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Federated user is now in the call + * 400: Conditions to join not met + * 404: Room not found + */ + public function joinFederatedCall(Room $room, Participant $participant, int $flags, bool $silent, bool $recordingConsent): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + 'flags' => $flags, + 'silent' => $silent, + 'recordingConsent' => $recordingConsent, + ]; + + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } + + /** + * @see \OCA\Talk\Controller\RoomController::updateFederatedCallFlags() + * + * @param Room $room the federated room to update the call flags in + * @param Participant $participant the federated user to update the call + * flags; the participant must have a session + * @param int<0, 15> $flags New flags + * @psalm-param int-mask-of $flags New flags + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: In-call flags updated successfully for federated user + * 400: Updating in-call flags is not possible + * 404: Room not found + */ + public function updateFederatedCallFlags(Room $room, Participant $participant, int $flags): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + 'flags' => $flags, + ]; + + $proxy = $this->proxy->put( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } + + /** + * @see \OCA\Talk\Controller\RoomController::leaveFederatedCall() + * + * @param Room $room the federated room to leave the call in + * @param Participant $participant the federated user that will leave the + * call; the participant must have a session + * @return DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Federated user left the call + * 404: Room not found + */ + public function leaveFederatedCall(Room $room, Participant $participant): DataResponse { + $options = [ + 'sessionId' => $participant->getSession()->getSessionId(), + ]; + + $proxy = $this->proxy->delete( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation', + $options, + ); + + $statusCode = $proxy->getStatusCode(); + if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode()); + throw new CannotReachRemoteException(); + } + + return new DataResponse([], $statusCode); + } +} diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index da0ad98f466..df467a67c69 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -8,7 +8,9 @@ namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier; +use OCA\Talk\Events\AActiveSinceModifiedEvent; use OCA\Talk\Events\AAttendeeRemovedEvent; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\ALobbyModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; use OCA\Talk\Events\LobbyModifiedEvent; @@ -34,17 +36,21 @@ public function __construct( } public function handle(Event $event): void { - if (!$event instanceof LobbyModifiedEvent + if (!$event instanceof ActiveSinceModifiedEvent + && !$event instanceof LobbyModifiedEvent && !$event instanceof RoomModifiedEvent) { return; } if (!in_array($event->getProperty(), [ + ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE, ARoomModifiedEvent::PROPERTY_AVATAR, ARoomModifiedEvent::PROPERTY_DESCRIPTION, + ARoomModifiedEvent::PROPERTY_IN_CALL, ARoomModifiedEvent::PROPERTY_LOBBY, ARoomModifiedEvent::PROPERTY_NAME, ARoomModifiedEvent::PROPERTY_READ_ONLY, + ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT, ARoomModifiedEvent::PROPERTY_TYPE, ], true)) { return; @@ -54,7 +60,9 @@ public function handle(Event $event): void { foreach ($participants as $participant) { $cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId()); - if ($event instanceof ALobbyModifiedEvent) { + if ($event instanceof AActiveSinceModifiedEvent) { + $success = $this->notifyActiveSinceModified($cloudId, $participant, $event); + } elseif ($event instanceof ALobbyModifiedEvent) { $success = $this->notifyLobbyModified($cloudId, $participant, $event); } else { $success = $this->notifyRoomModified($cloudId, $participant, $event); @@ -66,6 +74,19 @@ public function handle(Event $event): void { } } + private function notifyActiveSinceModified(ICloudId $cloudId, Participant $participant, AActiveSinceModifiedEvent $event) { + return $this->backendNotifier->sendRoomModifiedActiveSinceUpdate( + $cloudId->getRemote(), + $participant->getAttendee()->getId(), + $participant->getAttendee()->getAccessToken(), + $event->getRoom()->getToken(), + $event->getProperty(), + $event->getNewValue(), + $event->getOldValue(), + $event->getCallFlag(), + ); + } + private function notifyLobbyModified(ICloudId $cloudId, Participant $participant, ALobbyModifiedEvent $event) { return $this->backendNotifier->sendRoomModifiedLobbyUpdate( $cloudId->getRemote(), diff --git a/lib/Manager.php b/lib/Manager.php index 514536abc03..50a49fda55f 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -104,7 +104,6 @@ public function createRoomObjectFromData(array $data): Room { 'avatar' => '', 'remote_server' => '', 'remote_token' => '', - 'active_guests' => 0, 'default_permissions' => 0, 'call_permissions' => 0, 'call_flag' => 0, @@ -174,7 +173,6 @@ public function createRoomObject(array $row): Room { (string) $row['avatar'], (string) $row['remote_server'], (string) $row['remote_token'], - (int) $row['active_guests'], (int) $row['default_permissions'], (int) $row['call_permissions'], (int) $row['call_flag'], diff --git a/lib/Middleware/Attribute/RequireFederatedParticipant.php b/lib/Middleware/Attribute/RequireFederatedParticipant.php new file mode 100644 index 00000000000..48fe382462a --- /dev/null +++ b/lib/Middleware/Attribute/RequireFederatedParticipant.php @@ -0,0 +1,32 @@ +sessionIdParameter; + } +} diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index 0f4b296422b..1f2e79b6e35 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -19,6 +19,7 @@ use OCA\Talk\Middleware\Attribute\AllowWithoutParticipantWhenPendingInvitation; use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireAuthenticatedParticipant; +use OCA\Talk\Middleware\Attribute\RequireFederatedParticipant; use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant; use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; @@ -34,6 +35,7 @@ use OCA\Talk\Middleware\Exceptions\ReadOnlyException; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\InvitationMapper; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\BanService; @@ -118,6 +120,12 @@ public function beforeController(Controller $controller, string $methodName): vo $this->getLoggedInOrGuest($controller, false, true); } + $attributes = $reflectionMethod->getAttributes(RequireFederatedParticipant::class); + if (!empty($attributes)) { + $sessionIdParameter = $this->readSessionIdParameterFromAttributes($attributes); + $this->getLoggedInOrGuest($controller, false, sessionIdParameter: $sessionIdParameter); + } + if (!empty($reflectionMethod->getAttributes(RequireParticipant::class))) { $this->getLoggedInOrGuest($controller, false); } @@ -153,6 +161,17 @@ public function beforeController(Controller $controller, string $methodName): vo } } + protected function readSessionIdParameterFromAttributes(array $attributes): ?string { + foreach ($attributes as $attribute) { + /** @var RequireFederatedParticipant $instance */ + $instance = $attribute->newInstance(); + if ($instance->getSessionIdParameter() !== null) { + return $instance->getSessionIdParameter(); + } + } + return null; + } + /** * @param AEnvironmentAwareController $controller * @throws ForbiddenException @@ -193,22 +212,40 @@ protected function getLoggedIn(AEnvironmentAwareController $controller, bool $mo * @throws NotAModeratorException * @throws ParticipantNotFoundException */ - protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired, bool $requireListedWhenNoParticipant = false, bool $requireFederationWhenNotLoggedIn = false): void { + protected function getLoggedInOrGuest( + AEnvironmentAwareController $controller, + bool $moderatorRequired, + bool $requireListedWhenNoParticipant = false, + bool $requireFederationWhenNotLoggedIn = false, + ?string $sessionIdParameter = null + ): void { if ($requireFederationWhenNotLoggedIn && $this->userId === null && !$this->federationAuthenticator->isFederationRequest()) { throw new ParticipantNotFoundException(); } + if ($sessionIdParameter !== null && !$this->federationAuthenticator->isFederationRequest()) { + throw new ParticipantNotFoundException(); + } + $room = $controller->getRoom(); + $sessionId = null; if (!$room instanceof Room) { $token = $this->request->getParam('token'); - $sessionId = $this->talkSession->getSessionForRoom($token); if (!$this->federationAuthenticator->isFederationRequest()) { + $sessionId = $this->talkSession->getSessionForRoom($token); $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); } else { - $room = $this->manager->getRoomByRemoteAccess($token, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $this->federationAuthenticator->getAccessToken()); + $sessionId = $sessionIdParameter !== null ? $this->request->getParam($sessionIdParameter) : null; + $room = $this->manager->getRoomByRemoteAccess($token, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $this->federationAuthenticator->getAccessToken(), $sessionId); // Get and set the participant already, so we don't retry public access $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId()); + + if ($sessionIdParameter !== null && !$participant->getSession() instanceof Session) { + // If a session is required, fail if we didn't find it + throw new ParticipantNotFoundException(); + } + $this->federationAuthenticator->authenticated($room, $participant); $controller->setParticipant($participant); } diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 3dcf3a693ea..c3022c4445f 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -26,7 +26,6 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ->addSelect($alias . 'description') ->addSelect($alias . 'password') ->addSelect($alias . 'avatar') - ->addSelect($alias . 'active_guests') ->addSelect($alias . 'active_since') ->addSelect($alias . 'default_permissions') ->addSelect($alias . 'call_permissions') diff --git a/lib/Room.php b/lib/Room.php index bba1c88c6a9..fca31047949 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -111,7 +111,6 @@ public function __construct( private string $avatar, private string $remoteServer, private string $remoteToken, - private int $activeGuests, private int $defaultPermissions, private int $callPermissions, private int $callFlag, @@ -296,7 +295,6 @@ public function setDescription(string $description): void { } public function resetActiveSince(): void { - $this->activeGuests = 0; $this->activeSince = null; } @@ -500,14 +498,11 @@ public function getParticipant(?string $userId, $sessionId = null): Participant return $this->manager->createParticipantObject($this, $row); } - public function setActiveSince(\DateTime $since, int $callFlag, bool $isGuest): void { + public function setActiveSince(\DateTime $since, int $callFlag): void { if (!$this->activeSince) { $this->activeSince = $since; } $this->callFlag |= $callFlag; - if ($isGuest) { - $this->activeGuests++; - } } public function getBreakoutRoomMode(): int { diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index ccad235d61b..2b0f5127a10 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -142,6 +142,10 @@ public function formatRoomV4( 'mentionPermissions' => Room::MENTION_PERMISSIONS_EVERYONE, ]; + if ($room->isFederatedConversation()) { + $roomData['recordingConsent'] = $room->getRecordingConsent(); + } + $lastActivity = $room->getLastActivity(); if ($lastActivity instanceof \DateTimeInterface) { $lastActivity = $lastActivity->getTimestamp(); @@ -221,6 +225,10 @@ public function formatRoomV4( 'mentionPermissions' => $room->getMentionPermissions(), ]); + if ($room->isFederatedConversation()) { + $roomData['recordingConsent'] = $room->getRecordingConsent(); + } + if ($currentParticipant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { if (isset($commonReadMessages[$room->getId()])) { $roomData['lastCommonReadMessage'] = $commonReadMessages[$room->getId()]; diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 2ad9a904394..eec1459826f 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -10,7 +10,9 @@ use InvalidArgumentException; use OCA\Talk\Config; +use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; +use OCA\Talk\Events\BeforeActiveSinceModifiedEvent; use OCA\Talk\Events\BeforeLobbyModifiedEvent; use OCA\Talk\Events\BeforeRoomDeletedEvent; use OCA\Talk\Events\BeforeRoomModifiedEvent; @@ -832,9 +834,18 @@ public function setBreakoutRoomStatus(Room $room, int $status): bool { } public function resetActiveSince(Room $room): bool { + $oldActiveSince = $room->getActiveSince(); + $oldCallFlag = $room->getCallFlag(); + + if ($oldActiveSince === null && $oldCallFlag === Participant::FLAG_DISCONNECTED) { + return false; + } + + $event = new BeforeActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') - ->set('active_guests', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) ->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE)) ->set('call_flag', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) ->set('call_permissions', $update->createNamedParameter(Attendee::PERMISSIONS_DEFAULT, IQueryBuilder::PARAM_INT)) @@ -843,33 +854,45 @@ public function resetActiveSince(Room $room): bool { $room->resetActiveSince(); - return (bool) $update->executeStatement(); + $result = (bool) $update->executeStatement(); + + $event = new ActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + + return $result; } - public function setActiveSince(Room $room, \DateTime $since, int $callFlag, bool $isGuest): bool { - if ($isGuest && $room->getType() === Room::TYPE_PUBLIC) { - $update = $this->db->getQueryBuilder(); - $update->update('talk_rooms') - ->set('active_guests', $update->createFunction($update->getColumnName('active_guests') . ' + 1')) - ->set( - 'call_flag', - $update->expr()->bitwiseOr('call_flag', $callFlag) - ) - ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); - $update->executeStatement(); - } elseif (!$isGuest) { - $update = $this->db->getQueryBuilder(); - $update->update('talk_rooms') - ->set( - 'call_flag', - $update->expr()->bitwiseOr('call_flag', $callFlag) - ) - ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); - $update->executeStatement(); + public function setActiveSince(Room $room, \DateTime $since, int $callFlag): bool { + $oldActiveSince = $room->getActiveSince(); + $oldCallFlag = $room->getCallFlag(); + + if ($room->getActiveSince() instanceof \DateTime && $oldCallFlag === $callFlag) { + return false; } if ($room->getActiveSince() instanceof \DateTime) { - $room->setActiveSince($room->getActiveSince(), $callFlag, $isGuest); + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + } else { + $event = new BeforeActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + } + + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set( + 'call_flag', + $update->expr()->bitwiseOr('call_flag', $callFlag) + ) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + if ($room->getActiveSince() instanceof \DateTime) { + $room->setActiveSince($room->getActiveSince(), $callFlag); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); + return false; } @@ -880,7 +903,10 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag, bool ->andWhere($update->expr()->isNull('active_since')); $update->executeStatement(); - $room->setActiveSince($since, $callFlag, $isGuest); + $room->setActiveSince($since, $callFlag); + + $event = new ActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag); + $this->dispatcher->dispatchTyped($event); return true; } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index ddb4680dde8..f5f97624742 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -324,7 +324,8 @@ public function participantsModified(Room $room, array $sessionIds): void { foreach ($participants as $participant) { $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS - && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } @@ -336,7 +337,8 @@ public function participantsModified(Room $room, array $sessionIds): void { 'participantPermissions' => Attendee::PERMISSIONS_CUSTOM, 'displayName' => $attendee->getDisplayName(), ]; - if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + if ($attendee->getActorType() === Attendee::ACTOR_USERS + || $attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $data['userId'] = $attendee->getActorId(); } @@ -414,7 +416,8 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds, boo $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS - && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } @@ -426,7 +429,8 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds, boo 'participantType' => $attendee->getParticipantType(), 'participantPermissions' => $participant->getPermissions(), ]; - if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + if ($attendee->getActorType() === Attendee::ACTOR_USERS + || $attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $data['userId'] = $attendee->getActorId(); } diff --git a/openapi-full.json b/openapi-full.json index 1dc80a2a4e0..8fdf92ecb5a 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -4463,6 +4463,481 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + "post": { + "operationId": "call-join-federated-call", + "summary": "Join call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + }, + "flags": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "In-Call flags", + "minimum": 0, + "maximum": 15 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "Join the call silently" + }, + "recordingConsent": { + "type": "boolean", + "default": false, + "description": "Agreement to be recorded" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call joined successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Conditions to join not met", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Call not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "call-update-federated-call-flags", + "summary": "Update the in-call flags on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "flags" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to update the flags with" + }, + "flags": { + "type": "integer", + "format": "int64", + "description": "New flags", + "minimum": 0, + "maximum": 15 + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "In-call flags updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Updating in-call flags is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "call-leave-federated-call", + "summary": "Leave a call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to leave with" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call left successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { "post": { "operationId": "call-ring-attendee", diff --git a/openapi.json b/openapi.json index 368e5c05613..cfec14168c9 100644 --- a/openapi.json +++ b/openapi.json @@ -4350,6 +4350,481 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + "post": { + "operationId": "call-join-federated-call", + "summary": "Join call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to join with" + }, + "flags": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "In-Call flags", + "minimum": 0, + "maximum": 15 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "Join the call silently" + }, + "recordingConsent": { + "type": "boolean", + "default": false, + "description": "Agreement to be recorded" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call joined successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Conditions to join not met", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Call not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "call-update-federated-call-flags", + "summary": "Update the in-call flags on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "flags" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to update the flags with" + }, + "flags": { + "type": "integer", + "format": "int64", + "description": "New flags", + "minimum": 0, + "maximum": 15 + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "In-call flags updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "400": { + "description": "Updating in-call flags is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "call-leave-federated-call", + "summary": "Leave a call on the host server using the session id of the federated user.", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId" + ], + "properties": { + "sessionId": { + "type": "string", + "description": "Federated session id to leave with" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Call left successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Call session not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "nullable": true + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { "post": { "operationId": "call-ring-attendee", diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index ef9bc0fa792..10bdf3d9517 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -183,7 +183,7 @@ export default { }, showMediaSettingsToggle() { - return (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) + return (hasTalkFeature(this.token, 'federation-v2') || !hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) }, supportBanV1() { diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue index 67ae5da335e..8f1b4f0b942 100644 --- a/src/components/TopBar/CallButton.vue +++ b/src/components/TopBar/CallButton.vue @@ -309,7 +309,7 @@ export default { return this.callEnabled && this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF && this.conversation.readOnly === CONVERSATION.STATE.READ_WRITE - && (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) + && (hasTalkFeature(this.token, 'federation-v2') || !hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) && !this.isInCall }, diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 4626526b895..af99d8a532b 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -261,6 +261,25 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the in-call flags on the host server using the session id of the federated user. */ + put: operations["call-update-federated-call-flags"]; + /** Join call on the host server using the session id of the federated user. */ + post: operations["call-join-federated-call"]; + /** Leave a call on the host server using the session id of the federated user. */ + delete: operations["call-leave-federated-call"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { parameters: { query?: never; @@ -3377,6 +3396,212 @@ export interface operations { }; }; }; + "call-update-federated-call-flags": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to update the flags with */ + sessionId: string; + /** + * Format: int64 + * @description New flags + */ + flags: number; + }; + }; + }; + responses: { + /** @description In-call flags updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Updating in-call flags is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-join-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + /** + * Format: int64 + * @description In-Call flags + */ + flags?: number | null; + /** + * @description Join the call silently + * @default false + */ + silent?: boolean; + /** + * @description Agreement to be recorded + * @default false + */ + recordingConsent?: boolean; + }; + }; + }; + responses: { + /** @description Call joined successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Conditions to join not met */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error?: string; + }; + }; + }; + }; + }; + /** @description Call not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-leave-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Call left successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "call-ring-attendee": { parameters: { query?: never; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 8fe9d747304..a8cbbf6891d 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -261,6 +261,25 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the in-call flags on the host server using the session id of the federated user. */ + put: operations["call-update-federated-call-flags"]; + /** Join call on the host server using the session id of the federated user. */ + post: operations["call-join-federated-call"]; + /** Leave a call on the host server using the session id of the federated user. */ + delete: operations["call-leave-federated-call"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/ring/{attendeeId}": { parameters: { query?: never; @@ -2858,6 +2877,212 @@ export interface operations { }; }; }; + "call-update-federated-call-flags": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to update the flags with */ + sessionId: string; + /** + * Format: int64 + * @description New flags + */ + flags: number; + }; + }; + }; + responses: { + /** @description In-call flags updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Updating in-call flags is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-join-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to join with */ + sessionId: string; + /** + * Format: int64 + * @description In-Call flags + */ + flags?: number | null; + /** + * @description Join the call silently + * @default false + */ + silent?: boolean; + /** + * @description Agreement to be recorded + * @default false + */ + recordingConsent?: boolean; + }; + }; + }; + responses: { + /** @description Call joined successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Conditions to join not met */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error?: string; + }; + }; + }; + }; + }; + /** @description Call not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "call-leave-federated-call": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Federated session id to leave with */ + sessionId: string; + }; + }; + }; + responses: { + /** @description Call left successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Call session not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "call-ring-attendee": { parameters: { query?: never; diff --git a/tests/integration/features/federation/call.feature b/tests/integration/features/federation/call.feature new file mode 100644 index 00000000000..620a76c334b --- /dev/null +++ b/tests/integration/features/federation/call.feature @@ -0,0 +1,141 @@ +Feature: federation/call + + Background: + Given using server "REMOTE" + And user "participant2" exists + And the following "spreed" app config is set + | federation_enabled | yes | + And using server "LOCAL" + And user "participant1" exists + And the following "spreed" app config is set + | federation_enabled | yes | + + Scenario: join call + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And user "participant1" joins call "room" with 200 (v4) + | flags | 3 | + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 3 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 0 | + When user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + Then using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + | callFlag | + | 7 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 3 | + | federated_users | participant2@{$REMOTE_URL} | 7 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 7 | + + Scenario: update call flags + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 7 | + When user "participant2" updates call flags in room "LOCAL::room" to "1" with 200 (v4) + And using server "LOCAL" + And user "participant1" is participant of room "room" (v4) + | callFlag | + | 7 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 0 | + | federated_users | participant2@{$REMOTE_URL} | 1 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 1 | + + Scenario: leave call + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And user "participant1" joins call "room" with 200 (v4) + | flags | 3 | + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 7 | + When user "participant2" leaves call "LOCAL::room" with 200 (v4) + And using server "LOCAL" + And user "participant1" leaves call "room" with 200 (v4) + Then user "participant1" is participant of room "room" (v4) + | callFlag | + | 0 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 0 | + | federated_users | participant2@{$REMOTE_URL} | 0 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 0 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 0 | diff --git a/tests/php/Service/RoomServiceTest.php b/tests/php/Service/RoomServiceTest.php index 7307630cef9..776c0356514 100644 --- a/tests/php/Service/RoomServiceTest.php +++ b/tests/php/Service/RoomServiceTest.php @@ -350,7 +350,6 @@ public function testVerifyPassword(): void { '', '', '', - 0, Attendee::PERMISSIONS_DEFAULT, Attendee::PERMISSIONS_DEFAULT, Participant::FLAG_DISCONNECTED,