Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED
$participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED,
$participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS

To keep writing the data for now, while we fix reading it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guests are counted from the attendees instead of from the active_guests column for several years already, according to the PR:
Make the call summary reuse the attendee table also for guests, so we don't have "and 132 guests" when a guest rejoins with a weak internet connection.

Due to that and to either avoid propagating it for consistency, or not propagating it which would work for now if the field is used only for the call summary but could cause unexpected inconsistencies in the future if it ends used for something else, I kept it removed and did not change it.

And if it needs to be introduced back later... we can always do it anyway :-)

);
}

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