* * 200: List of peers in the call returned */ #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequireReadWriteConversation] public function getPeersForCall(): DataResponse { 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->getPeersForCall($this->room, $this->participant); } $timeout = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT; $result = []; $participants = $this->participantService->getParticipantsInCall($this->room, $timeout); foreach ($participants as $participant) { $displayName = $participant->getAttendee()->getActorId(); if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { if ($participant->getAttendee()->getDisplayName()) { $displayName = $participant->getAttendee()->getDisplayName(); } else { $userDisplayName = $this->userManager->getDisplayName($participant->getAttendee()->getActorId()); if ($userDisplayName !== null) { $displayName = $userDisplayName; } } } else { $displayName = $participant->getAttendee()->getDisplayName(); } $result[] = [ 'actorType' => $participant->getAttendee()->getActorType(), 'actorId' => $participant->getAttendee()->getActorId(), 'displayName' => $displayName, 'token' => $this->room->getToken(), 'lastPing' => $participant->getSession()->getLastPing(), 'sessionId' => $participant->getSession()->getSessionId(), ]; } return new DataResponse($result); } /** * Join a call * * @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 When the user ticked a checkbox and agreed with being recorded * (Only needed when the `config => call => recording-consent` capability is set to {@see RecordingService::CONSENT_REQUIRED_YES} * or the capability is {@see RecordingService::CONSENT_REQUIRED_OPTIONAL} * and the conversation `recordingConsent` value is {@see RecordingService::CONSENT_REQUIRED_YES} ) * @return DataResponse, array{}>|DataResponse * * 200: Call joined successfully * 400: No recording consent was given * 404: Call not found */ #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequireReadWriteConversation] public function joinCall(?int $flags = null, bool $silent = false, bool $recordingConsent = false): DataResponse { try { $this->validateRecordingConsent($recordingConsent); } catch (\InvalidArgumentException) { return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); } $this->participantService->ensureOneToOneRoomIsFilled($this->room); $session = $this->participant->getSession(); if (!$session instanceof Session) { return new DataResponse([], Http::STATUS_NOT_FOUND); } if ($flags === null) { // Default flags: user is in room with audio/video. $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); $response = $proxy->joinFederatedCall($this->room, $this->participant, $flags, $silent, $recordingConsent); if ($response->getStatus() === Http::STATUS_OK) { $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); } return $response; } try { $this->participantService->changeInCall($this->room, $this->participant, $flags, silent: $silent); $this->roomService->setActiveSince($this->room, $this->participant, $this->timeFactory->getDateTime(), $flags, silent: $silent); } catch (\InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } 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); } try { $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); $this->roomService->setActiveSince($this->room, $this->participant, $this->timeFactory->getDateTime(), $flags, silent: $silent); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } return new DataResponse(); } /** * Ring an attendee * * @param int $attendeeId ID of the attendee to ring * @return DataResponse, array{}>|DataResponse * * 200: Attendee rang successfully * 400: Ringing attendee is not possible * 404: Attendee could not be found */ #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::START_CALL)] public function ringAttendee(int $attendeeId): DataResponse { 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->ringAttendee($this->room, $this->participant, $attendeeId); } if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } if ($this->participant->getSession() && $this->participant->getSession()->getInCall() === Participant::FLAG_DISCONNECTED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } try { $this->participantService->sendCallNotificationForAttendee($this->room, $this->participant, $attendeeId); } catch (\InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } catch (DoesNotExistException) { return new DataResponse([], Http::STATUS_NOT_FOUND); } return new DataResponse(); } /** * Call a SIP dial-out attendee * * @param int $attendeeId ID of the attendee to call * @return DataResponse * * 201: Dial-out initiated successfully * 400: SIP dial-out not possible * 404: Participant could not be found or is a wrong type * 501: SIP dial-out is not configured on the server */ #[PublicPage] #[RequireCallEnabled] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::START_CALL)] public function sipDialOut(int $attendeeId): DataResponse { if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } if ($this->participant->getSession() && $this->participant->getSession()->getInCall() === Participant::FLAG_DISCONNECTED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } try { $this->participantService->startDialOutRequest($this->dialOutService, $this->room, $attendeeId); } catch (ParticipantNotFoundException) { return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (DialOutFailedException $e) { return new DataResponse([ 'error' => $e->getMessage(), 'message' => $e->getReadableError(), ], Http::STATUS_NOT_IMPLEMENTED); } catch (\InvalidArgumentException) { return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED); } return new DataResponse([], Http::STATUS_CREATED); } /** * Update the in-call flags * * @param int<0, 15> $flags New flags * @psalm-param int-mask-of $flags New flags * @return DataResponse, array{}> * * 200: In-call flags updated successfully * 400: Updating in-call flags is not possible * 404: Call session not found */ #[FederationSupported] #[PublicPage] #[RequireParticipant] public function updateCallFlags(int $flags): DataResponse { $session = $this->participant->getSession(); if (!$session instanceof Session) { 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); $response = $proxy->updateFederatedCallFlags($this->room, $this->participant, $flags); if ($response->getStatus() === Http::STATUS_OK) { $this->participantService->updateCallFlags($this->room, $this->participant, $flags); } return $response; } try { $this->participantService->updateCallFlags($this->room, $this->participant, $flags); } catch (\Exception $exception) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } 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 * * @param bool $all whether to also terminate the call for all participants * @return DataResponse, array{}> * * 200: Call left successfully * 404: Call session not found */ #[FederationSupported] #[PublicPage] #[RequireParticipant] public function leaveCall(bool $all = false): DataResponse { $session = $this->participant->getSession(); if (!$session instanceof Session) { 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); $response = $proxy->leaveFederatedCall($this->room, $this->participant); if ($response->getStatus() === Http::STATUS_OK) { $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); } return $response; } if ($all && $this->participant->hasModeratorPermissions()) { $result = $this->roomService->resetActiveSinceInDatabaseOnly($this->room); if (!$result) { // Someone else won the race condition, make sure this user disconnects directly and then return $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); return new DataResponse(); } $this->participantService->endCallForEveryone($this->room, $this->participant); $this->roomService->resetActiveSinceInModelOnly($this->room); } else { $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); if (!$this->participantService->hasActiveSessionsInCall($this->room)) { $this->roomService->resetActiveSince($this->room, $this->participant); } } 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); if (!$this->participantService->hasActiveSessionsInCall($this->room)) { $this->roomService->resetActiveSince($this->room, $this->participant); } return new DataResponse(); } }