Browse Source

Merge pull request #10635 from nextcloud/feat/10408/recording-consent-version-2

feat(recording): ⏺️ Add API for recording consent Version2
pull/10649/head
Joas Schilling 2 years ago
committed by GitHub
parent
commit
c1999911fd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      appinfo/info.xml
  2. 2
      appinfo/routes/routesRoomController.php
  3. 10
      docs/call.md
  4. 2
      docs/capabilities.md
  5. 5
      docs/constants.md
  6. 20
      docs/conversation.md
  7. 1
      docs/settings.md
  8. 13
      lib/Capabilities.php
  9. 23
      lib/Config.php
  10. 23
      lib/Controller/CallController.php
  11. 32
      lib/Controller/RoomController.php
  12. 27
      lib/Events/BeforeRoomModifiedEvent.php
  13. 27
      lib/Events/RoomModifiedEvent.php
  14. 4
      lib/Manager.php
  15. 55
      lib/Migration/Version18000Date20231005091416.php
  16. 1
      lib/Model/SelectHelper.php
  17. 1
      lib/ResponseDefinitions.php
  18. 20
      lib/Room.php
  19. 4
      lib/Service/RecordingService.php
  20. 2
      lib/Service/RoomFormatter.php
  21. 37
      lib/Service/RoomService.php
  22. 1
      lib/Settings/Admin/AdminSettings.php
  23. 194
      openapi.json
  24. 14
      tests/integration/features/bootstrap/FeatureContext.php
  25. 48
      tests/integration/features/callapi/recording.feature
  26. 3
      tests/php/CapabilitiesTest.php
  27. 11
      tests/php/Service/RoomServiceTest.php

2
appinfo/info.xml

@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
]]></description>
<version>18.0.0-dev.6</version>
<version>18.0.0-dev.7</version>
<licence>agpl</licence>
<author>Daniel Calviño Sánchez</author>

2
appinfo/routes/routesRoomController.php

@ -110,6 +110,8 @@ return [
['name' => 'Room#setLobby', 'url' => '/api/{apiVersion}/room/{token}/webinar/lobby', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setSIPEnabled() */
['name' => 'Room#setSIPEnabled', 'url' => '/api/{apiVersion}/room/{token}/webinar/sip', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setRecordingConsent() */
['name' => 'Room#setRecordingConsent', 'url' => '/api/{apiVersion}/room/{token}/recording-consent', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setMessageExpiration() */
['name' => 'Room#setMessageExpiration', 'url' => '/api/{apiVersion}/room/{token}/message-expiration', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
],

10
docs/call.md

@ -40,14 +40,16 @@
* Endpoint: `/call/{token}`
* Data:
| field | type | Description |
|----------|------|----------------------------------------------------------------------------------------------------------------------------------------|
| `flags` | int | Flags what streams are provided by the participant (see [Constants - Participant in-call flag](constants.md#participant-in-call-flag)) |
| `silent` | bool | Disable start call notifications for group/public calls |
| field | type | Description |
|--------------------|------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `flags` | int | Flags what streams are provided by the participant (see [Constants - Participant in-call flag](constants.md#participant-in-call-flag)) |
| `silent` | bool | Disable start call notifications for group/public calls |
| `recordingConsent` | bool | When the user ticked a checkbox and agreed with being recorded (Only needed when the `config => call => recording-consent` capability is set to `1` or the capability is `2` and the conversation `recordingConsent` value is `1`) |
* Response:
- Status code:
+ `200 OK`
+ `400 Bad Request` When recording consent is required but was not given
+ `403 Forbidden` When the conversation is read-only
+ `404 Not Found` When the conversation could not be found for the participant
+ `404 Not Found` When the user did not join the conversation before

2
docs/capabilities.md

@ -130,4 +130,6 @@
## 18
* `session-state` - Sessions can mark themselves as inactive, so the participant receives notifications again
* `note-to-self` - Support for "Note-to-self" conversation exists
* `recording-consent` - Whether admins and moderators can require recording consent before joining a call
* `config => chat => has-translation-providers` - When true, translation tuples can be loaded from the [OCS Translation API](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-translation-api.html#get-available-translation-options).
* `config => call => recording-consent` - Whether users need to consent into call recording before joining a call (see [constants list](constants.md#recording-consent-required))

5
docs/constants.md

@ -130,6 +130,11 @@
* `4` - Starting audio recording
* `5` - Recording failed
### Recording consent required
* `0` - No recording consent is required to join a call
* `1` - Recording consent is required
* `2` - Recording consent can be enabled by moderators on conversation level
## Chat
### Shared item types

20
docs/conversation.md

@ -107,6 +107,7 @@
| `isCustomAvatar` | bool | v4 | | Flag if the conversation has a custom avatar (only available with `avatar` capability) |
| `callStartTime` | int | v4 | | Timestamp when the call was started (only available with `recording-v1` capability) |
| `callRecording` | int | v4 | | Type of call recording (see [Constants - Call recording status](constants.md#call-recording-status)) (only available with `recording-v1` capability) |
| `recordingConsent` | int | v4 | | Whether recording consent is required before joining a call (see [constants list](constants.md#recording-consent-required)) (only available with `recording-consent` capability) |
## Creating a new conversation
@ -440,6 +441,25 @@ Get all (for moderators and in case of "free selection") or the assigned breakou
+ `403 Forbidden` When the current user is not a moderator, owner or guest moderator
+ `404 Not Found` When the conversation could not be found for the participant
## Set recording consent
* Required capability: `recording-consent`
* Method: `PUT`
* Endpoint: `/room/{token}/recording-consent`
* Data:
| field | type | Description |
|--------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `recordingConsent` | int | New consent setting for the conversation (Only `0` and `1` from the [constants](constants.md#recording-consent-required) are allowed here.) |
* Response:
- Status code:
+ `200 OK`
+ `400 Bad Request` When the consent value is invalid
+ `400 Bad Request` When the consent is being enabled while a call is going on
+ `403 Forbidden` When the current user is not a moderator/owner
+ `404 Not Found` When the conversation could not be found for the participant
## Open a conversation
* Required capability: `listable-rooms`

1
docs/settings.md

@ -86,6 +86,7 @@ Legend:
| `token_entropy` | int | `8` | No | | Length of conversation tokens, can be increased to make tokens harder to guess but reduces readability and dial-in comfort |
| `default_group_notification` | int | `2` | No | 🖌️ | Default notification level for group conversations [constants list](constants.md#participant-notification-levels) |
| `default_permissions` | int | `246` | Yes | | Default permissions for non-moderators (see [constants list](constants.md#attendee-permissions) for bit flags) |
| `recording_consent` | int | `0` | Yes | 🖌️ | Whether users have to agree on being recorded before they can join the call (see [constants](constants.md#recording-consent-required)) |
| `grid_videos_limit` | int | `19` | No | | Maximum number of videos to show (additional to the own video) |
| `grid_videos_limit_enforced` | string<br>`yes` or `no` | `no` | No | | Whether the number of grid videos should be enforced |
| `changelog` | string<br>`yes` or `no` | `yes` | No | | Whether the changelog conversation is updated with new features on major releases |

13
lib/Capabilities.php

@ -66,6 +66,7 @@ class Capabilities implements IPublicCapability {
* enabled: bool,
* breakout-rooms: bool,
* recording: bool,
* recording-consent: int,
* supported-reactions: string[],
* predefined-backgrounds: string[],
* can-upload-background: bool,
@ -163,6 +164,7 @@ class Capabilities implements IPublicCapability {
'markdown-messages',
'session-state',
'note-to-self',
'recording-consent',
],
'config' => [
'attachments' => [
@ -172,6 +174,7 @@ class Capabilities implements IPublicCapability {
'enabled' => ((int) $this->serverConfig->getAppValue('spreed', 'start_calls', (string) Room::START_CALL_EVERYONE)) !== Room::START_CALL_NOONE,
'breakout-rooms' => $this->talkConfig->isBreakoutRoomsEnabled(),
'recording' => $this->talkConfig->isRecordingEnabled(),
'recording-consent' => $this->talkConfig->recordingConsentRequired(),
'supported-reactions' => ['❤️', '🎉', '👏', '👍', '👎', '😂', '🤩', '🤔', '😲', '😥'],
],
'chat' => [
@ -216,14 +219,18 @@ class Capabilities implements IPublicCapability {
$capabilities['features'][] = 'chat-reference-id';
}
$predefinedBackgrounds = $this->talkCache->get('predefined_backgrounds');
if ($predefinedBackgrounds !== null) {
/** @var ?string[] $predefinedBackgrounds */
$predefinedBackgrounds = null;
$cachedPredefinedBackgrounds = $this->talkCache->get('predefined_backgrounds');
if ($cachedPredefinedBackgrounds !== null) {
// Try using cached value
$predefinedBackgrounds = json_decode($predefinedBackgrounds, true);
/** @var string[]|null $predefinedBackgrounds */
$predefinedBackgrounds = json_decode($cachedPredefinedBackgrounds, true);
}
if (!is_array($predefinedBackgrounds)) {
// Cache was empty or invalid, regenerate
/** @var string[] $predefinedBackgrounds */
$predefinedBackgrounds = [];
if (file_exists(__DIR__ . '/../img/backgrounds')) {
$directoryIterator = new \DirectoryIterator(__DIR__ . '/../img/backgrounds');

23
lib/Config.php

@ -25,6 +25,7 @@ namespace OCA\Talk;
use OCA\Talk\Events\GetTurnServersEvent;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Vendor\Firebase\JWT\JWT;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
@ -182,6 +183,28 @@ class Config {
return true;
}
/**
* @return RecordingService::CONSENT_REQUIRED_*
*/
public function recordingConsentRequired(): int {
if (!$this->isRecordingEnabled()) {
return RecordingService::CONSENT_REQUIRED_NO;
}
return $this->getRecordingConsentConfig();
}
/**
* @return RecordingService::CONSENT_REQUIRED_*
*/
public function getRecordingConsentConfig(): int {
return match ((int) $this->config->getAppValue('spreed', 'recording_consent', (string) RecordingService::CONSENT_REQUIRED_NO)) {
RecordingService::CONSENT_REQUIRED_YES => RecordingService::CONSENT_REQUIRED_YES,
RecordingService::CONSENT_REQUIRED_OPTIONAL => RecordingService::CONSENT_REQUIRED_OPTIONAL,
default => RecordingService::CONSENT_REQUIRED_NO,
};
}
public function getRecordingFolder(string $userId): string {
return $this->config->getUserValue(
$userId,

23
lib/Controller/CallController.php

@ -28,6 +28,7 @@ declare(strict_types=1);
namespace OCA\Talk\Controller;
use OCA\Talk\Config;
use OCA\Talk\Middleware\Attribute\RequireCallEnabled;
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
use OCA\Talk\Middleware\Attribute\RequireParticipant;
@ -38,6 +39,7 @@ use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\PublicPage;
@ -58,6 +60,7 @@ class CallController extends AEnvironmentAwareController {
private RoomService $roomService,
private IUserManager $userManager,
private ITimeFactory $timeFactory,
private Config $talkConfig,
) {
parent::__construct($appName, $request);
}
@ -113,10 +116,14 @@ class CallController extends AEnvironmentAwareController {
* @param int|null $flags In-Call flags
* @param int|null $forcePermissions In-call permissions
* @param bool $silent Join the call silently
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @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<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>
*
* 200: Call joined successfully
* 400: Failed to join the call
* 400: No recording consent was given
* 404: Call not found
*/
#[PublicPage]
@ -124,7 +131,17 @@ class CallController extends AEnvironmentAwareController {
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequireReadWriteConversation]
public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $silent = false): DataResponse {
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);
}
}
$this->participantService->ensureOneToOneRoomIsFilled($this->room);
$session = $this->participant->getSession();

32
lib/Controller/RoomController.php

@ -56,6 +56,7 @@ use OCA\Talk\Service\BreakoutRoomService;
use OCA\Talk\Service\ChecksumVerificationService;
use OCA\Talk\Service\NoteToSelfService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Service\RoomFormatter;
use OCA\Talk\Service\RoomService;
use OCA\Talk\Service\SessionService;
@ -151,6 +152,7 @@ class RoomController extends AEnvironmentAwareController {
$this->config->getAppValue('spreed', 'sip_bridge_groups', '[]') . '#' .
$this->config->getAppValue('spreed', 'sip_bridge_dialin_info') . '#' .
$this->config->getAppValue('spreed', 'sip_bridge_shared_secret') . '#' .
$this->config->getAppValue('spreed', 'recording_consent') . '#' .
$this->config->getAppValue('theming', 'cachebuster', '1')
)];
}
@ -1775,6 +1777,34 @@ class RoomController extends AEnvironmentAwareController {
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
* Set recording consent requirement for this conversation
*
* @param int $recordingConsent New consent setting for the conversation
* (Only {@see RecordingService::CONSENT_REQUIRED_NO} and {@see RecordingService::CONSENT_REQUIRED_YES} are allowed here.)
* @psalm-param RecordingService::CONSENT_REQUIRED_NO|RecordingService::CONSENT_REQUIRED_YES $recordingConsent
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_PRECONDITION_FAILED, array<empty>, array{}>
*
* 200: Recording consent requirement set successfully
* 400: Setting recording consent requirement is not possible
* 412: No recording server is configured
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
public function setRecordingConsent(int $recordingConsent): DataResponse {
if (!$this->talkConfig->isRecordingEnabled()) {
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
}
try {
$this->roomService->setRecordingConsent($this->room, $recordingConsent);
} catch (\InvalidArgumentException $exception) {
return new DataResponse(['error' => $exception->getMessage()], Http::STATUS_BAD_REQUEST);
}
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
* Resend invitations
*
@ -1830,7 +1860,7 @@ class RoomController extends AEnvironmentAwareController {
try {
$this->roomService->setMessageExpiration($this->room, $seconds);
} catch (\InvalidArgumentException $exception) {
return new DataResponse(['error' => $exception], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => $exception->getMessage()], Http::STATUS_BAD_REQUEST);
}
return new DataResponse();

27
lib/Events/BeforeRoomModifiedEvent.php

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Events;
class BeforeRoomModifiedEvent extends ModifyRoomEvent {
}

27
lib/Events/RoomModifiedEvent.php

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Events;
class RoomModifiedEvent extends ModifyRoomEvent {
}

4
lib/Manager.php

@ -132,6 +132,7 @@ class Manager {
'breakout_room_mode' => 0,
'breakout_room_status' => 0,
'call_recording' => 0,
'recording_consent' => 0,
], $data));
}
@ -198,7 +199,8 @@ class Manager {
(string) $row['object_id'],
(int) $row['breakout_room_mode'],
(int) $row['breakout_room_status'],
(int) $row['call_recording']
(int) $row['call_recording'],
(int) $row['recording_consent'],
);
}

55
lib/Migration/Version18000Date20231005091416.php

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version18000Date20231005091416 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('talk_rooms');
if (!$table->hasColumn('recording_consent')) {
$table->addColumn('recording_consent', Types::SMALLINT, [
'default' => 0,
'unsigned' => true,
]);
return $schema;
}
return null;
}
}

1
lib/Model/SelectHelper.php

@ -58,6 +58,7 @@ class SelectHelper {
->addSelect($alias . 'breakout_room_mode')
->addSelect($alias . 'breakout_room_status')
->addSelect($alias . 'call_recording')
->addSelect($alias . 'recording_consent')
->selectAlias($alias . 'id', 'r_id');
}

1
lib/ResponseDefinitions.php

@ -211,6 +211,7 @@ namespace OCA\Talk;
* participantType: int,
* permissions: int,
* readOnly: int,
* recordingConsent: int,
* sessionId: string,
* sipEnabled: int,
* status?: string,

20
lib/Room.php

@ -33,6 +33,7 @@ use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\SelectHelper;
use OCA\Talk\Model\Session;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
@ -167,6 +168,9 @@ class Room {
protected ?string $currentUser = null;
protected ?Participant $participant = null;
/**
* @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
*/
public function __construct(
private Manager $manager,
private IDBConnection $db,
@ -201,6 +205,7 @@ class Room {
private int $breakoutRoomMode,
private int $breakoutRoomStatus,
private int $callRecording,
private int $recordingConsent,
) {
}
@ -589,4 +594,19 @@ class Room {
public function setCallRecording(int $callRecording): void {
$this->callRecording = $callRecording;
}
/**
* @return RecordingService::CONSENT_REQUIRED_*
*/
public function getRecordingConsent(): int {
return $this->recordingConsent;
}
/**
* @param int $recordingConsent
* @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
*/
public function setRecordingConsent(int $recordingConsent): void {
$this->recordingConsent = $recordingConsent;
}
}

4
lib/Service/RecordingService.php

@ -53,6 +53,10 @@ use OCP\SpeechToText\ISpeechToTextManager;
use Psr\Log\LoggerInterface;
class RecordingService {
public const CONSENT_REQUIRED_NO = 0;
public const CONSENT_REQUIRED_YES = 1;
public const CONSENT_REQUIRED_OPTIONAL = 2;
public const DEFAULT_ALLOWED_RECORDING_FORMATS = [
'audio/ogg' => ['ogg'],
'video/ogg' => ['ogv'],

2
lib/Service/RoomFormatter.php

@ -152,6 +152,7 @@ class RoomFormatter {
'isCustomAvatar' => $this->avatarService->isCustomAvatar($room),
'breakoutRoomMode' => BreakoutRoom::MODE_NOT_CONFIGURED,
'breakoutRoomStatus' => BreakoutRoom::STATUS_STOPPED,
'recordingConsent' => $this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL ? $room->getRecordingConsent() : $this->talkConfig->recordingConsentRequired(),
];
$lastActivity = $room->getLastActivity();
@ -209,6 +210,7 @@ class RoomFormatter {
'hasCall' => $room->getActiveSince() instanceof \DateTimeInterface,
'callStartTime' => $room->getActiveSince() instanceof \DateTimeInterface ? $room->getActiveSince()->getTimestamp() : 0,
'callRecording' => $room->getCallRecording(),
'recordingConsent' => $this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL ? $room->getRecordingConsent() : $this->talkConfig->recordingConsentRequired(),
'lastActivity' => $lastActivity,
'callFlag' => $room->getCallFlag(),
'isFavorite' => $attendee->isFavorite(),

37
lib/Service/RoomService.php

@ -25,9 +25,11 @@ namespace OCA\Talk\Service;
use InvalidArgumentException;
use OCA\Talk\Config;
use OCA\Talk\Events\BeforeRoomModifiedEvent;
use OCA\Talk\Events\ModifyLobbyEvent;
use OCA\Talk\Events\ModifyRoomEvent;
use OCA\Talk\Events\RoomEvent;
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Events\VerifyRoomPasswordEvent;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Manager;
@ -269,6 +271,41 @@ class RoomService {
return true;
}
/**
* @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
* @throws \InvalidArgumentException When the room has an active call or the value is invalid
*/
public function setRecordingConsent(Room $room, int $recordingConsent): void {
$oldRecordingConsent = $room->getRecordingConsent();
if ($recordingConsent === $oldRecordingConsent) {
return;
}
if (!in_array($recordingConsent, [RecordingService::CONSENT_REQUIRED_NO, RecordingService::CONSENT_REQUIRED_YES], true)) {
throw new InvalidArgumentException('value');
}
if ($recordingConsent !== RecordingService::CONSENT_REQUIRED_NO && $room->getCallFlag() !== Participant::FLAG_DISCONNECTED) {
throw new InvalidArgumentException('call');
}
$event = new BeforeRoomModifiedEvent($room, 'recordingConsent', $recordingConsent, $oldRecordingConsent);
$this->dispatcher->dispatchTyped($event);
$update = $this->db->getQueryBuilder();
$update->update('talk_rooms')
->set('recording_consent', $update->createNamedParameter($recordingConsent, IQueryBuilder::PARAM_INT))
->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
$update->executeStatement();
$room->setRecordingConsent($recordingConsent);
$event = new RoomModifiedEvent($room, 'recordingConsent', $recordingConsent, $oldRecordingConsent);
$this->dispatcher->dispatchTyped($event);
}
/**
* @param string $newName Currently it is only allowed to rename: self::TYPE_GROUP, self::TYPE_PUBLIC
* @param string|null $oldName

1
lib/Settings/Admin/AdminSettings.php

@ -464,6 +464,7 @@ class AdminSettings implements ISettings {
'secret' => $this->talkConfig->getRecordingSecret(),
'uploadLimit' => is_infinite($uploadLimit) ? 0 : $uploadLimit,
]);
$this->initialState->provideInitialState('recording_consent', $this->talkConfig->getRecordingConsentConfig());
}
protected function initSIPBridge(): void {

194
openapi.json

@ -646,6 +646,7 @@
"enabled",
"breakout-rooms",
"recording",
"recording-consent",
"supported-reactions",
"predefined-backgrounds",
"can-upload-background"
@ -660,6 +661,10 @@
"recording": {
"type": "boolean"
},
"recording-consent": {
"type": "integer",
"format": "int64"
},
"supported-reactions": {
"type": "array",
"items": {
@ -819,6 +824,7 @@
"participantType",
"permissions",
"readOnly",
"recordingConsent",
"sessionId",
"sipEnabled",
"token",
@ -984,6 +990,10 @@
"type": "integer",
"format": "int64"
},
"recordingConsent": {
"type": "integer",
"format": "int64"
},
"sessionId": {
"type": "string"
},
@ -3981,6 +3991,15 @@
"default": 0
}
},
{
"name": "recordingConsent",
"in": "query",
"description": "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} )",
"schema": {
"type": "integer",
"default": 0
}
},
{
"name": "apiVersion",
"in": "path",
@ -4042,8 +4061,8 @@
}
}
},
"400": {
"description": "Failed to join the call",
"404": {
"description": "Call not found",
"content": {
"application/json": {
"schema": {
@ -4070,8 +4089,8 @@
}
}
},
"404": {
"description": "Call not found",
"400": {
"description": "No recording consent was given",
"content": {
"application/json": {
"schema": {
@ -4090,7 +4109,14 @@
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
"data": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
@ -15140,6 +15166,164 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/recording-consent": {
"put": {
"operationId": "room-set-recording-consent",
"summary": "Set recording consent requirement for this conversation",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "recordingConsent",
"in": "query",
"description": "New consent setting for the conversation (Only {@see RecordingService::CONSENT_REQUIRED_NO} and {@see RecordingService::CONSENT_REQUIRED_YES} are allowed here.)",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"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": "Recording consent requirement set successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Room"
}
}
}
}
}
}
}
},
"400": {
"description": "Setting recording consent requirement 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": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"412": {
"description": "No recording server is configured",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/message-expiration": {
"post": {
"operationId": "room-set-message-expiration",

14
tests/integration/features/bootstrap/FeatureContext.php

@ -440,6 +440,9 @@ class FeatureContext implements Context, SnippetAcceptingContext {
if (isset($expectedRoom['callRecording'])) {
$data['callRecording'] = (int) $room['callRecording'];
}
if (isset($expectedRoom['recordingConsent'])) {
$data['recordingConsent'] = (int) $room['recordingConsent'];
}
if (isset($expectedRoom['participants'])) {
throw new \Exception('participants key needs to be checked via participants endpoint');
}
@ -3443,6 +3446,17 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$this->assertStatusCode($this->response, $statusCode);
}
/**
* @Given /^user "([^"]*)" sets the recording consent to (\d+) for room "([^"]*)" with (\d+) \((v4)\)$/
*/
public function userSetsTheRecordingConsentToXWithStatusCode(string $user, int $recordingConsent, string $identifier, int $statusCode, string $apiVersion = 'v4'): void {
$this->setCurrentUser($user);
$this->sendRequest('PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/recording-consent', [
'recordingConsent' => $recordingConsent,
]);
$this->assertStatusCode($this->response, $statusCode);
}
/**
* @When /^wait for ([0-9]+) (second|seconds)$/
*/

48
tests/integration/features/callapi/recording.feature

@ -546,3 +546,51 @@ Feature: callapi/recording
And user "participant1" is participant of the following unordered rooms (v4)
| type | name | callRecording |
| 2 | room1 | 0 |
Scenario: Recording consent required by admin
Given recording server is started
And the following "spreed" app config is set
| recording_consent | 1 |
And user "participant1" creates room "room1" (v4)
| roomType | 2 |
| roomName | room1 |
And user "participant1" joins room "room1" with 200 (v4)
And user "participant1" joins call "room1" with 400 (v4)
And user "participant1" joins call "room1" with 400 (v4)
| recordingConsent | 0 |
And user "participant1" joins call "room1" with 200 (v4)
| recordingConsent | 1 |
Scenario: Recording consent optional by admin enabled by moderator
Given recording server is started
And the following "spreed" app config is set
| recording_consent | 2 |
And user "participant1" creates room "room1" (v4)
| roomType | 2 |
| roomName | room1 |
And user "participant1" joins room "room1" with 200 (v4)
And user "participant1" joins call "room1" with 200 (v4)
# Can not enable consent when a call is on-going …
When user "participant1" sets the recording consent to 1 for room "room1" with 400 (v4)
Then user "participant1" is participant of the following unordered rooms (v4)
| type | name | recordingConsent |
| 2 | room1 | 0 |
When user "participant1" leaves call "room1" with 200 (v4)
And user "participant1" sets the recording consent to 1 for room "room1" with 200 (v4)
Then user "participant1" is participant of the following unordered rooms (v4)
| type | name | recordingConsent |
| 2 | room1 | 1 |
And user "participant1" joins call "room1" with 400 (v4)
And user "participant1" joins call "room1" with 200 (v4)
| recordingConsent | 1 |
# … but can disable consent when a call is on-going
When user "participant1" sets the recording consent to 0 for room "room1" with 200 (v4)
Then user "participant1" is participant of the following unordered rooms (v4)
| type | name | recordingConsent |
| 2 | room1 | 0 |
When user "participant1" leaves call "room1" with 200 (v4)
# Invalid value on conversation level
When user "participant1" sets the recording consent to 2 for room "room1" with 400 (v4)
Then user "participant1" is participant of the following unordered rooms (v4)
| type | name | recordingConsent |
| 2 | room1 | 0 |

3
tests/php/CapabilitiesTest.php

@ -141,6 +141,7 @@ class CapabilitiesTest extends TestCase {
'markdown-messages',
'session-state',
'note-to-self',
'recording-consent',
'message-expiration',
'reactions',
];
@ -190,6 +191,7 @@ class CapabilitiesTest extends TestCase {
'enabled' => true,
'breakout-rooms' => false,
'recording' => false,
'recording-consent' => 0,
'supported-reactions' => ['❤️', '🎉', '👏', '👍', '👎', '😂', '🤩', '🤔', '😲', '😥'],
'predefined-backgrounds' => [
'1_office.jpg',
@ -314,6 +316,7 @@ class CapabilitiesTest extends TestCase {
'enabled' => false,
'breakout-rooms' => true,
'recording' => false,
'recording-consent' => 0,
'supported-reactions' => ['❤️', '🎉', '👏', '👍', '👎', '😂', '🤩', '🤔', '😲', '😥'],
'predefined-backgrounds' => [
'1_office.jpg',

11
tests/php/Service/RoomServiceTest.php

@ -30,9 +30,11 @@ use OCA\Talk\Events\VerifyRoomPasswordEvent;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\BreakoutRoom;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Service\RoomService;
use OCA\Talk\Webinary;
use OCP\AppFramework\Utility\ITimeFactory;
@ -374,7 +376,7 @@ class RoomServiceTest extends TestCase {
Room::LISTABLE_NONE,
0,
Webinary::LOBBY_NONE,
0,
Webinary::SIP_DISABLED,
null,
'foobar',
'Test',
@ -394,9 +396,10 @@ class RoomServiceTest extends TestCase {
null,
'',
'',
0,
0,
0
BreakoutRoom::MODE_NOT_CONFIGURED,
BreakoutRoom::STATUS_STOPPED,
Room::RECORDING_NONE,
RecordingService::CONSENT_REQUIRED_NO,
);
$verificationResult = $service->verifyPassword($room, '1234');

Loading…
Cancel
Save