Browse Source

feat(recording): Recording consent API - Version 2

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/10635/head
Joas Schilling 2 years ago
parent
commit
4d292fe959
No known key found for this signature in database GPG Key ID: 74434EFE0D2E2205
  1. 2
      appinfo/info.xml
  2. 2
      appinfo/routes/routesRoomController.php
  3. 10
      docs/call.md
  4. 1
      docs/capabilities.md
  5. 5
      docs/constants.md
  6. 20
      docs/conversation.md
  7. 2
      docs/settings.md
  8. 12
      lib/Capabilities.php
  9. 20
      lib/Config.php
  10. 16
      lib/Controller/CallController.php
  11. 31
      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. 5
      lib/Service/RecordingService.php
  20. 2
      lib/Service/RoomFormatter.php
  21. 37
      lib/Service/RoomService.php
  22. 194
      openapi.json
  23. 14
      tests/integration/features/bootstrap/FeatureContext.php
  24. 48
      tests/integration/features/callapi/recording.feature
  25. 3
      tests/php/CapabilitiesTest.php
  26. 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,11 +40,11 @@
* 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 |
| `recordingConsent` | bool | When the user ticked a checkbox and agreed with being recorded (Only needed for certain states of the `config => call => recording-consent` capability |
| 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:

1
docs/capabilities.md

@ -130,5 +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

@ -131,8 +131,9 @@
* `5` - Recording failed
### Recording consent required
* `no` - No recording consent is required to join a call
* `yes` - Recording consent is required on admin level
* `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

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`

2
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 |
@ -93,7 +94,6 @@ Legend:
| `hide_signaling_warning` | string<br>`yes` or `no` | `no` | No | 🖌️ | Flag that allows to suppress the warning that an HPB should be configured |
| `breakout_rooms` | string<br>`yes` or `no` | `yes` | Yes | | Whether or not breakout rooms are allowed (Will only prevent creating new breakout rooms. Existing conversations are not modified.) |
| `call_recording` | string<br>`yes` or `no` | `yes` | Yes | | Enable call recording |
| `recording_consent` | string<br>`yes` or `no` | `no` | Yes | | When enabled users have to agree on being recorded before they can join the call |
| `call_recording_transcription` | string<br>`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcripted when a transcription provider is enabled. |
| `federation_enabled` | string<br>`yes` or `no` | `no` | Yes | | 🏗️ *Work in progress:* Whether or not federation with this instance is allowed |
| `conversations_files` | string<br>`1` or `0` | `1` | No | 🖌️ | Whether the files app integration is enabled allowing to start conversations in the right sidebar |

12
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' => [
@ -217,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');

20
lib/Config.php

@ -186,18 +186,20 @@ class Config {
/**
* @return RecordingService::CONSENT_REQUIRED_*
*/
public function recordingConsentRequired(): string {
if ($this->isRecordingEnabled()) {
public function recordingConsentRequired(): int {
if (!$this->isRecordingEnabled()) {
return RecordingService::CONSENT_REQUIRED_NO;
}
switch ($this->config->getAppValue('spreed', 'recording_consent', 'no')) {
case RecordingService::CONSENT_REQUIRED_YES:
return RecordingService::CONSENT_REQUIRED_YES;
case RecordingService::CONSENT_REQUIRED_NO:
default:
return RecordingService::CONSENT_REQUIRED_NO;
}
return match ($this->getRecordingConsentConfig()) {
RecordingService::CONSENT_REQUIRED_YES => RecordingService::CONSENT_REQUIRED_YES,
RecordingService::CONSENT_REQUIRED_OPTIONAL => RecordingService::CONSENT_REQUIRED_OPTIONAL,
default => RecordingService::CONSENT_REQUIRED_NO,
};
}
protected function getRecordingConsentConfig(): int {
return (int) $this->config->getAppValue('spreed', 'recording_consent', (string) RecordingService::CONSENT_REQUIRED_NO);
}
public function getRecordingFolder(string $userId): string {

16
lib/Controller/CallController.php

@ -116,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]
@ -129,7 +133,13 @@ class CallController extends AEnvironmentAwareController {
#[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) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
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);

31
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;
@ -1776,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
*
@ -1831,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;
}
}

5
lib/Service/RecordingService.php

@ -53,8 +53,9 @@ use OCP\SpeechToText\ISpeechToTextManager;
use Psr\Log\LoggerInterface;
class RecordingService {
public const CONSENT_REQUIRED_NO = 'no';
public const CONSENT_REQUIRED_YES = 'yes';
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'],

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

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