*/ public function getPublicSettings(): array { $internal = $this->getInternalSettings(); unset($internal['talkServiceAppPassword']); $internal['talkServiceConfigured'] = $this->hasResolvedServiceCredentials(); $internal['manualTalkServiceConfigured'] = $this->hasManualServiceCredentials(); $internal['automationAccountSources'] = $this->getAutomationAccountSources(); return $internal; } /** @return array */ public function getInternalSettings(): array { return [ 'enabled' => $this->getBool(self::KEY_ENABLED, false), 'automationCredentialSource' => $this->getEnum(self::KEY_AUTOMATION_CREDENTIAL_SOURCE, 'manual', ['manual', 'env_admin', 'env_service']), 'talkServiceUser' => $this->getString(self::KEY_TALK_SERVICE_USER, ''), 'talkServiceAppPassword' => $this->getString(self::KEY_TALK_SERVICE_APP_PASSWORD, ''), 'workspaceFilterMode' => $this->getEnum(self::KEY_WORKSPACE_FILTER_MODE, 'disabled', ['all', 'allowlist', 'selected', 'disabled']), 'workspaceAllowlist' => $this->getAllowlist(), 'deckSyncEnabled' => $this->getBool(self::KEY_DECK_SYNC_ENABLED, true), 'fileMirrorEnabled' => $this->getBool(self::KEY_FILE_MIRROR_ENABLED, true), 'fileMirrorMode' => $this->getEnum(self::KEY_FILE_MIRROR_MODE, 'copy', ['copy', 'link']), 'reactionWorkflowEnabled' => $this->getBool(self::KEY_REACTION_WORKFLOW_ENABLED, true), 'botCommandsEnabled' => $this->getBool(self::KEY_BOT_COMMANDS_ENABLED, false), 'summaryEnabled' => $this->getBool(self::KEY_SUMMARY_ENABLED, false), 'syncIntervalMinutes' => $this->getInt(self::KEY_SYNC_INTERVAL_MINUTES, 5, 1, 1440), ]; } /** @param array $payload */ /** @return array */ public function saveFromArray(array $payload): array { $current = $this->getInternalSettings(); $enabled = $this->coerceBool($payload['enabled'] ?? $current['enabled'], (bool)$current['enabled']); $deckSyncEnabled = $this->coerceBool($payload['deckSyncEnabled'] ?? $current['deckSyncEnabled'], (bool)$current['deckSyncEnabled']); $fileMirrorEnabled = $this->coerceBool($payload['fileMirrorEnabled'] ?? $current['fileMirrorEnabled'], (bool)$current['fileMirrorEnabled']); $reactionWorkflowEnabled = $this->coerceBool($payload['reactionWorkflowEnabled'] ?? $current['reactionWorkflowEnabled'], (bool)$current['reactionWorkflowEnabled']); $botCommandsEnabled = $this->coerceBool($payload['botCommandsEnabled'] ?? $current['botCommandsEnabled'], (bool)$current['botCommandsEnabled']); $summaryEnabled = $this->coerceBool($payload['summaryEnabled'] ?? $current['summaryEnabled'], (bool)$current['summaryEnabled']); $automationCredentialSource = $this->sanitizeEnum((string)($payload['automationCredentialSource'] ?? $current['automationCredentialSource']), ['manual', 'env_admin', 'env_service'], 'manual'); $talkServiceUser = $this->limitString((string)($payload['talkServiceUser'] ?? $current['talkServiceUser']), 256); $workspaceFilterMode = $this->sanitizeEnum((string)($payload['workspaceFilterMode'] ?? $current['workspaceFilterMode']), ['all', 'allowlist', 'selected', 'disabled'], 'disabled'); $fileMirrorMode = $this->sanitizeEnum((string)($payload['fileMirrorMode'] ?? $current['fileMirrorMode']), ['copy', 'link'], 'copy'); $workspaceAllowlist = $this->sanitizeAllowlist($payload['workspaceAllowlist'] ?? $current['workspaceAllowlist']); $syncIntervalMinutes = $this->coerceInt($payload['syncIntervalMinutes'] ?? $current['syncIntervalMinutes'], 5, 1, 1440); $talkServiceAppPassword = (string)$current['talkServiceAppPassword']; if (array_key_exists('talkServiceAppPassword', $payload)) { $newPassword = trim((string)$payload['talkServiceAppPassword']); if ($newPassword !== '') { $talkServiceAppPassword = $newPassword; } } if (!empty($payload['clearTalkServiceAppPassword'])) { $talkServiceAppPassword = ''; } $this->setBool(self::KEY_ENABLED, $enabled); $this->setString(self::KEY_AUTOMATION_CREDENTIAL_SOURCE, $automationCredentialSource); $this->setString(self::KEY_TALK_SERVICE_USER, $talkServiceUser); $this->setString(self::KEY_TALK_SERVICE_APP_PASSWORD, $talkServiceAppPassword); $this->setString(self::KEY_WORKSPACE_FILTER_MODE, $workspaceFilterMode); $this->setJson(self::KEY_WORKSPACE_ALLOWLIST, $workspaceAllowlist); $this->setBool(self::KEY_DECK_SYNC_ENABLED, $deckSyncEnabled); $this->setBool(self::KEY_FILE_MIRROR_ENABLED, $fileMirrorEnabled); $this->setString(self::KEY_FILE_MIRROR_MODE, $fileMirrorMode); $this->setBool(self::KEY_REACTION_WORKFLOW_ENABLED, $reactionWorkflowEnabled); $this->setBool(self::KEY_BOT_COMMANDS_ENABLED, $botCommandsEnabled); $this->setBool(self::KEY_SUMMARY_ENABLED, $summaryEnabled); $this->setString(self::KEY_SYNC_INTERVAL_MINUTES, (string)$syncIntervalMinutes); return $this->getPublicSettings(); } public function isRunnable(): bool { $settings = $this->getInternalSettings(); return (bool)$settings['enabled'] && $this->hasResolvedServiceCredentials() && (string)$settings['workspaceFilterMode'] !== 'disabled'; } /** @return array{user: string, password: string, source: string} */ public function getAutomationCredentials(): array { $settings = $this->getInternalSettings(); $source = (string)$settings['automationCredentialSource']; if ($source === 'env_admin') { return [ 'user' => $this->getEnvString('NEXTCLOUD_ADMIN_USER'), 'password' => $this->getEnvString('NEXTCLOUD_ADMIN_PASSWORD'), 'source' => $source, ]; } if ($source === 'env_service') { return [ 'user' => $this->getEnvString('NEXTCLOUD_SERVICE_USER'), 'password' => $this->getEnvString('NEXTCLOUD_SERVICE_PASSWORD'), 'source' => $source, ]; } return [ 'user' => trim((string)$settings['talkServiceUser']), 'password' => trim((string)$settings['talkServiceAppPassword']), 'source' => 'manual', ]; } public function getAutomationUserId(): string { return $this->getAutomationCredentials()['user']; } private function hasResolvedServiceCredentials(): bool { $credentials = $this->getAutomationCredentials(); return $credentials['user'] !== '' && $credentials['password'] !== ''; } private function hasManualServiceCredentials(): bool { return $this->getString(self::KEY_TALK_SERVICE_USER, '') !== '' && $this->getString(self::KEY_TALK_SERVICE_APP_PASSWORD, '') !== ''; } /** @return array> */ private function getAutomationAccountSources(): array { $adminUser = $this->getEnvString('NEXTCLOUD_ADMIN_USER'); $adminPassword = $this->getEnvString('NEXTCLOUD_ADMIN_PASSWORD'); $serviceUser = $this->getEnvString('NEXTCLOUD_SERVICE_USER'); $servicePassword = $this->getEnvString('NEXTCLOUD_SERVICE_PASSWORD'); return [ 'manual' => [ 'user' => $this->getString(self::KEY_TALK_SERVICE_USER, ''), 'configured' => $this->hasManualServiceCredentials(), ], 'env_admin' => [ 'user' => $adminUser, 'configured' => $adminUser !== '' && $adminPassword !== '', ], 'env_service' => [ 'user' => $serviceUser, 'configured' => $serviceUser !== '' && $servicePassword !== '', ], ]; } private function getEnvString(string $key): string { $value = getenv($key); if ($value === false && isset($_SERVER[$key])) { $value = $_SERVER[$key]; } if ($value === false && isset($_ENV[$key])) { $value = $_ENV[$key]; } return trim((string)($value === false ? '' : $value)); } private function getString(string $key, string $default): string { return trim((string)$this->config->getAppValue(Application::APP_ID, $key, $default)); } private function setString(string $key, string $value): void { $this->config->setAppValue(Application::APP_ID, $key, $value); } private function getBool(string $key, bool $default): bool { return $this->config->getAppValue(Application::APP_ID, $key, $default ? '1' : '0') === '1'; } private function setBool(string $key, bool $value): void { $this->config->setAppValue(Application::APP_ID, $key, $value ? '1' : '0'); } /** @param array $allowed */ private function getEnum(string $key, string $default, array $allowed): string { return $this->sanitizeEnum($this->getString($key, $default), $allowed, $default); } /** @param array $allowed */ private function sanitizeEnum(string $value, array $allowed, string $default): string { $value = strtolower(trim($value)); return in_array($value, $allowed, true) ? $value : $default; } private function getInt(string $key, int $default, int $min, int $max): int { return $this->coerceInt($this->getString($key, (string)$default), $default, $min, $max); } private function coerceInt(mixed $value, int $default, int $min, int $max): int { $int = filter_var($value, FILTER_VALIDATE_INT); if ($int === false) { $int = $default; } return max($min, min($max, (int)$int)); } private function coerceBool(mixed $value, bool $default): bool { $bool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); return $bool ?? $default; } /** @return array */ private function getAllowlist(): array { $raw = $this->config->getAppValue(Application::APP_ID, self::KEY_WORKSPACE_ALLOWLIST, '[]'); try { $decoded = json_decode((string)$raw, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable) { return []; } return $this->sanitizeAllowlist($decoded); } /** @return array */ private function sanitizeAllowlist(mixed $value): array { if (is_string($value)) { $value = preg_split('/[\r\n,]+/', $value) ?: []; } if (!is_array($value)) { return []; } $items = []; foreach ($value as $entry) { $item = $this->limitString((string)$entry, 255); if ($item === '') { continue; } $items[$item] = $item; } return array_values($items); } /** @param array $value */ private function setJson(string $key, array $value): void { $this->config->setAppValue( Application::APP_ID, $key, json_encode($value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) ); } private function limitString(string $value, int $maxLength): string { $value = trim($value); if (function_exists('mb_substr')) { return mb_substr($value, 0, $maxLength); } return substr($value, 0, $maxLength); } }