Skip to content

Commit

Permalink
Merge pull request #13488 from nextcloud/feat/13446/api-for-media-pre…
Browse files Browse the repository at this point in the history
…ference

feat(calls): Add appconfig and user preference for default setting of…
  • Loading branch information
nickvergessen authored Oct 8, 2024
2 parents d25f504 + af917bc commit ac44f1d
Show file tree
Hide file tree
Showing 28 changed files with 211 additions and 64 deletions.
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@

## 20.1
* `archived-conversations` (local) - Conversations can be marked as archived which will hide them from the conversation list by default
* `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation
16 changes: 11 additions & 5 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@

## User settings

| Key | Capability | Default | Valid values |
|-----------------------|------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| `attachment_folder` | `config => attachments => folder` | Value of app config `default_attachment_folder` | Path owned by the user to store uploads and received shares. It is created if it does not exist. |
| `read_status_privacy` | `config => chat => read-privacy` | `0` | One of the read-status constants from the [constants list](constants.md#participant-read-status-privacy) |
| `typing_privacy` | `config => chat => typing-privacy` | `0` | One of the typing privacy constants from the [constants list](constants.md#participant-typing-privacy) |
**Note:** Settings from `calls_start_without_media` onwards can not be set via above API.
Instead, the server API `POST /ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}` needs to be used.

| Key | Capability | Default | Valid values |
|-----------------------------|-----------------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| `attachment_folder` | `config => attachments => folder` | Value of app config `default_attachment_folder` | Path owned by the user to store uploads and received shares. It is created if it does not exist. |
| `read_status_privacy` | `config => chat => read-privacy` | `0` | One of the read-status constants from the [constants list](constants.md#participant-read-status-privacy) |
| `typing_privacy` | `config => chat => typing-privacy` | `0` | One of the typing privacy constants from the [constants list](constants.md#participant-typing-privacy) |
| `play_sounds` | | `'yes'` | `'yes'` and `'no'` |
| `calls_start_without_media` | `config => call => start-without-media` | `''` falling back to app config with the same name | `'yes'` and `'no'` |

## Set SIP settings

Expand Down Expand Up @@ -94,6 +99,7 @@ Legend:
| `changelog` | string<br>`yes` or `no` | `yes` | No | | Whether the changelog conversation is updated with new features on major releases |
| `has_reference_id` | string<br>`yes` or `no` | `no` | Yes | | Indicator whether the clients can use the reference value to identify their message, will be automatically set to `yes` when the repair steps are executed |
| `hide_signaling_warning` | string<br>`yes` or `no` | `no` | No | 🖌️ | Flag that allows to suppress the warning that an HPB should be configured |
| `calls_start_without_media` | string<br>`yes` or `no` | `no` | Yes | | Whether participants start with enabled or disabled audio and video by default |
| `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 |
| `call_recording_transcription` | string<br>`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcripted when a transcription provider is enabled. |
Expand Down
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
use OCA\Talk\Search\MessageSearch;
use OCA\Talk\Search\UnifiedSearchCSSLoader;
use OCA\Talk\Search\UnifiedSearchFilterPlugin;
use OCA\Talk\Settings\BeforePreferenceSetEventListener;
use OCA\Talk\Settings\Personal;
use OCA\Talk\SetupCheck\BackgroundBlurLoading;
use OCA\Talk\SetupCheck\FederationLockCache;
Expand All @@ -124,6 +125,7 @@
use OCP\Collaboration\AutoComplete\AutoCompleteFilterEvent;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
Expand Down Expand Up @@ -177,6 +179,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareTemplateLoader::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareAuthTemplateLoader::class);
$context->registerEventListener(LoadSidebar::class, FilesTemplateLoader::class);
$context->registerEventListener(BeforePreferenceSetEvent::class, BeforePreferenceSetEventListener::class);

// Activity listeners
$context->registerEventListener(AttendeesAddedEvent::class, ActivityListener::class);
Expand Down
2 changes: 2 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class Capabilities implements IPublicCapability {
'call' => [
'predefined-backgrounds',
'can-upload-background',
'start-without-media',
],
'chat' => [
'read-privacy',
Expand Down Expand Up @@ -196,6 +197,7 @@ public function getCapabilities(): array {
'sip-enabled' => $this->talkConfig->isSIPConfigured(),
'sip-dialout-enabled' => $this->talkConfig->isSIPDialOutEnabled(),
'can-enable-sip' => false,
'start-without-media' => $this->talkConfig->getCallsStartWithoutMedia($user?->getUID()),
],
'chat' => [
'max-length' => ChatManager::MAX_CHAT_LENGTH,
Expand Down
18 changes: 18 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Settings\UserPreference;
use OCA\Talk\Vendor\Firebase\JWT\JWT;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
Expand Down Expand Up @@ -662,4 +663,21 @@ public function getGridVideosLimit(): int {
public function getGridVideosLimitEnforced(): bool {
return $this->config->getAppValue('spreed', 'grid_videos_limit_enforced', 'no') === 'yes';
}

/**
* User setting falling back to admin defined app config
*
* @param ?string $userId
* @return bool
*/
public function getCallsStartWithoutMedia(?string $userId): bool {
if ($userId !== null) {
$userSetting = $this->config->getUserValue($userId, 'spreed', UserPreference::CALLS_START_WITHOUT_MEDIA);
if ($userSetting === 'yes' || $userSetting === 'no') {
return $userSetting === 'yes';
}
}

return $this->appConfig->getAppValueBool('calls_start_without_media');
}
}
3 changes: 3 additions & 0 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2436,6 +2436,9 @@ public function getCapabilities(): DataResponse {
if (isset($data['config']['chat']['typing-privacy'])) {
$data['config']['chat']['typing-privacy'] = Participant::PRIVACY_PRIVATE;
}
if (isset($data['config']['call']['start-without-media'])) {
$data['config']['call']['start-without-media'] = $this->talkConfig->getCallsStartWithoutMedia($this->userId);
}

if ($response->getHeaders()['X-Nextcloud-Talk-Hash']) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $response->getHeaders()['X-Nextcloud-Talk-Hash'];
Expand Down
54 changes: 3 additions & 51 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,13 @@

namespace OCA\Talk\Controller;

use OCA\Files_Sharing\SharedStorage;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Settings\BeforePreferenceSetEventListener;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
Expand All @@ -35,8 +29,8 @@ public function __construct(
protected IRootFolder $rootFolder,
protected IConfig $config,
protected IGroupManager $groupManager,
protected ParticipantService $participantService,
protected LoggerInterface $logger,
protected BeforePreferenceSetEventListener $preferenceListener,
protected ?string $userId,
) {
parent::__construct($appName, $request);
Expand All @@ -54,57 +48,15 @@ public function __construct(
*/
#[NoAdminRequired]
public function setUserSetting(string $key, $value): DataResponse {
if (!$this->validateUserSetting($key, $value)) {
if (!$this->preferenceListener->validatePreference($this->userId, $key, $value)) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

$this->config->setUserValue($this->userId, 'spreed', $key, $value);

if ($key === 'read_status_privacy') {
$this->participantService->updateReadPrivacyForActor(Attendee::ACTOR_USERS, $this->userId, (int)$value);
}

return new DataResponse();
}

/**
* @param string $setting
* @param int|null|string $value
* @return bool
*/
protected function validateUserSetting(string $setting, $value): bool {
if ($setting === 'attachment_folder') {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
$node = $userFolder->get($value);
if (!$node instanceof Folder) {
throw new NotPermittedException('Node is not a directory');
}
if ($node->isShared()) {
throw new NotPermittedException('Folder is shared');
}
return !$node->getStorage()->instanceOfStorage(SharedStorage::class);
} catch (NotFoundException $e) {
$userFolder->newFolder($value);
return true;
} catch (NotPermittedException $e) {
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
return false;
}

if ($setting === 'typing_privacy' || $setting === 'read_status_privacy') {
return (int)$value === Participant::PRIVACY_PUBLIC ||
(int)$value === Participant::PRIVACY_PRIVATE;
}
if ($setting === 'play_sounds') {
return $value === 'yes' || $value === 'no';
}

return false;
}

/**
* Update SIP bridge settings
*
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@
* sip-enabled: bool,
* sip-dialout-enabled: bool,
* can-enable-sip: bool,
* start-without-media: bool,
* },
* chat: array{
* max-length: int,
Expand Down
101 changes: 101 additions & 0 deletions lib/Settings/BeforePreferenceSetEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

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

namespace OCA\Talk\Settings;

use OCA\Files_Sharing\SharedStorage;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Service\ParticipantService;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<Event>
*/
class BeforePreferenceSetEventListener implements IEventListener {
public function __construct(
protected IRootFolder $rootFolder,
protected ParticipantService $participantService,
protected LoggerInterface $logger,
) {
}

public function handle(Event $event): void {
if (!($event instanceof BeforePreferenceSetEvent)) {
// Unrelated
return;
}

if ($event->getAppId() !== Application::APP_ID) {
return;
}

$event->setValid($this->validatePreference(
$event->getUserId(),
$event->getConfigKey(),
$event->getConfigValue(),
));
}

/**
* @internal Make private/protected once SettingsController route was removed
*/
public function validatePreference(string $userId, string $key, string $value): bool {
if ($key === 'attachment_folder') {
return $this->validateAttachmentFolder($userId, $value);
}

// "boolean" yes/no
if ($key === UserPreference::CALLS_START_WITHOUT_MEDIA
|| $key === UserPreference::PLAY_SOUNDS) {
return $value === 'yes' || $value === 'no';
}

// "privacy" 0/1
if ($key === UserPreference::TYPING_PRIVACY
|| $key === UserPreference::READ_STATUS_PRIVACY) {
$valid = $value === (string)Participant::PRIVACY_PRIVATE || $value === (string)Participant::PRIVACY_PUBLIC;

if ($valid && $key === 'read_status_privacy') {
$this->participantService->updateReadPrivacyForActor(Attendee::ACTOR_USERS, $userId, (int)$value);
}
return $valid;
}

return false;
}

protected function validateAttachmentFolder(string $userId, string $value): bool {
try {
$userFolder = $this->rootFolder->getUserFolder($userId);
$node = $userFolder->get($value);
if (!$node instanceof Folder) {
throw new NotPermittedException('Node is not a directory');
}
if ($node->isShared()) {
throw new NotPermittedException('Folder is shared');
}
return !$node->getStorage()->instanceOfStorage(SharedStorage::class);
} catch (NotFoundException) {
$userFolder->newFolder($value);
return true;
} catch (NotPermittedException) {
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
return false;
}
}
16 changes: 16 additions & 0 deletions lib/Settings/UserPreference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

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

namespace OCA\Talk\Settings;

class UserPreference {
public const CALLS_START_WITHOUT_MEDIA = 'calls_start_without_media';
public const PLAY_SOUNDS = 'play_sounds';
public const TYPING_PRIVACY = 'typing_privacy';
public const READ_STATUS_PRIVACY = 'read_status_privacy';
}
6 changes: 5 additions & 1 deletion openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@
"can-upload-background",
"sip-enabled",
"sip-dialout-enabled",
"can-enable-sip"
"can-enable-sip",
"start-without-media"
],
"properties": {
"enabled": {
Expand Down Expand Up @@ -186,6 +187,9 @@
},
"can-enable-sip": {
"type": "boolean"
},
"start-without-media": {
"type": "boolean"
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion openapi-backend-recording.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"can-upload-background",
"sip-enabled",
"sip-dialout-enabled",
"can-enable-sip"
"can-enable-sip",
"start-without-media"
],
"properties": {
"enabled": {
Expand Down Expand Up @@ -119,6 +120,9 @@
},
"can-enable-sip": {
"type": "boolean"
},
"start-without-media": {
"type": "boolean"
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion openapi-backend-signaling.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"can-upload-background",
"sip-enabled",
"sip-dialout-enabled",
"can-enable-sip"
"can-enable-sip",
"start-without-media"
],
"properties": {
"enabled": {
Expand Down Expand Up @@ -119,6 +120,9 @@
},
"can-enable-sip": {
"type": "boolean"
},
"start-without-media": {
"type": "boolean"
}
}
},
Expand Down
Loading

0 comments on commit ac44f1d

Please sign in to comment.