diff --git a/docs/capabilities.md b/docs/capabilities.md index 7fdd4f639b3..b7a26f7fe16 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -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 diff --git a/docs/settings.md b/docs/settings.md index c1d0e61848d..6093c41af7e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -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 @@ -94,6 +99,7 @@ Legend: | `changelog` | string
`yes` or `no` | `yes` | No | | Whether the changelog conversation is updated with new features on major releases | | `has_reference_id` | string
`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
`yes` or `no` | `no` | No | 🖌️ | Flag that allows to suppress the warning that an HPB should be configured | +| `calls_start_without_media` | string
`yes` or `no` | `no` | Yes | | Whether participants start with enabled or disabled audio and video by default | | `breakout_rooms` | string
`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
`yes` or `no` | `yes` | Yes | | Enable call recording | | `call_recording_transcription` | string
`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcripted when a transcription provider is enabled. | diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index eb0eb165f8b..903aa418bf8 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -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; @@ -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; @@ -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); diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 0cff6f89ae4..24fe017da7d 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -127,6 +127,7 @@ class Capabilities implements IPublicCapability { 'call' => [ 'predefined-backgrounds', 'can-upload-background', + 'start-without-media', ], 'chat' => [ 'read-privacy', @@ -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, diff --git a/lib/Config.php b/lib/Config.php index 37c4198e619..2019731d976 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -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; @@ -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'); + } } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index d8b47b9604f..08ff87eff08 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -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']; diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 1989d90f8ee..6dc4abef085 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -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; @@ -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); @@ -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 * diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 7344e078f6c..7f55a770b35 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -339,6 +339,7 @@ * sip-enabled: bool, * sip-dialout-enabled: bool, * can-enable-sip: bool, + * start-without-media: bool, * }, * chat: array{ * max-length: int, diff --git a/lib/Settings/BeforePreferenceSetEventListener.php b/lib/Settings/BeforePreferenceSetEventListener.php new file mode 100644 index 00000000000..31196b10014 --- /dev/null +++ b/lib/Settings/BeforePreferenceSetEventListener.php @@ -0,0 +1,101 @@ + + */ +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; + } +} diff --git a/lib/Settings/UserPreference.php b/lib/Settings/UserPreference.php new file mode 100644 index 00000000000..f360172f20e --- /dev/null +++ b/lib/Settings/UserPreference.php @@ -0,0 +1,16 @@ + false, 'sip-dialout-enabled' => false, 'can-enable-sip' => false, + 'start-without-media' => false, 'predefined-backgrounds' => [ '1_office.jpg', '2_home.jpg', @@ -252,6 +253,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea 'sip-enabled' => false, 'sip-dialout-enabled' => false, 'can-enable-sip' => false, + 'start-without-media' => false, 'predefined-backgrounds' => [ '1_office.jpg', '2_home.jpg',