Skip to content

Commit

Permalink
Merge pull request #12668 from nextcloud/add-support-for-federated-calls
Browse files Browse the repository at this point in the history
Add support for federated calls
  • Loading branch information
danxuliu authored Jul 26, 2024
2 parents 7cf9d68 + 13f092a commit 02eb9c2
Show file tree
Hide file tree
Showing 29 changed files with 2,144 additions and 64 deletions.
6 changes: 6 additions & 0 deletions appinfo/routes/routesCallController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
],
];
6 changes: 6 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions lib/Activity/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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
);
}

Expand Down
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
165 changes: 154 additions & 11 deletions lib/Controller/CallController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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<Participant::FLAG_*>|null $flags
* @param bool $silent Join the call silently
* @param bool $recordingConsent Agreement to be recorded
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 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
*
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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<Participant::FLAG_*> $flags New flags
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST, array<empty>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 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
*
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 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();
}
}
36 changes: 36 additions & 0 deletions lib/Events/AActiveSinceModifiedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Events;

use OCA\Talk\Room;

abstract class AActiveSinceModifiedEvent extends ARoomModifiedEvent {
public function __construct(
Room $room,
?\DateTime $newValue,
?\DateTime $oldValue,
protected int $callFlag,
protected int $oldCallFlag,
) {
parent::__construct(
$room,
self::PROPERTY_ACTIVE_SINCE,
$newValue,
$oldValue,
);
}

public function getCallFlag(): int {
return $this->callFlag;
}

public function getOldCallFlag(): int {
return $this->oldCallFlag;
}
}
Loading

0 comments on commit 02eb9c2

Please sign in to comment.