package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.MutableStateFlow import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.PartialSettingsType import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.getAttribute import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.user.requireUser import xyz.nulldev.ts.config.GlobalConfigManager import java.io.File private fun validateValue( exception: Exception, validate: () -> Boolean, ) { if (!validate()) { throw exception } } private fun validateValue( value: T?, exception: Exception, validate: (value: T) -> Boolean, ) { if (value != null) { validateValue(exception) { validate(value) } } } private fun validateValue( value: T?, name: String, validate: (value: T) -> Boolean, ) { validateValue(value, Exception("Invalid value for \"$name\" [$value]"), validate) } private fun validateFilePath( value: String?, name: String, ) { validateValue(value, name) { File(it).exists() } } class SettingsMutation { data class SetSettingsInput( val clientMutationId: String? = null, val settings: PartialSettingsType, ) data class SetSettingsPayload( val clientMutationId: String?, val settings: SettingsType, ) private fun validateSettings(settings: Settings) { validateValue(settings.ip, "ip") { it.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$".toRegex()) } // proxy validateValue(settings.socksProxyVersion, "socksProxyVersion") { it == 4 || it == 5 } // webUI validateFilePath(settings.electronPath, "electronPath") validateValue(settings.webUIUpdateCheckInterval, "webUIUpdateCheckInterval") { it == 0.0 || it in 1.0..23.0 } // downloader validateFilePath(settings.downloadsPath, "downloadsPath") validateValue(settings.autoDownloadNewChaptersLimit, "autoDownloadNewChaptersLimit") { it >= 0 } // extensions validateValue(settings.extensionRepos, "extensionRepos") { it.all { repoUrl -> repoUrl.matches(repoMatchRegex) } } // requests validateValue(settings.maxSourcesInParallel, "maxSourcesInParallel") { it in 1..20 } // updater validateValue(settings.globalUpdateInterval, "globalUpdateInterval") { it == 0.0 || it >= 6 } // misc validateValue(settings.maxLogFiles, "maxLogFiles") { it >= 0 } val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)$".toRegex() validateValue(settings.maxLogFileSize, "maxLogFolderSize") { it.matches(logbackSizePattern) } validateValue(settings.maxLogFolderSize, "maxLogFolderSize") { it.matches(logbackSizePattern) } // backup validateFilePath(settings.backupPath, "backupPath") validateValue(settings.backupTime, "backupTime") { it.matches("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$".toRegex()) } validateValue(settings.backupInterval, "backupInterval") { it == 0 || it >= 1 } validateValue(settings.backupTTL, "backupTTL") { it == 0 || it >= 1 } // local source validateFilePath(settings.localSourcePath, "localSourcePath") // opds validateValue(settings.opdsItemsPerPage, "opdsItemsPerPage") { it in 10..5000 } } private fun updateSetting( newSetting: SettingType?, configSetting: MutableStateFlow, ) { if (newSetting == null) { return } configSetting.value = newSetting } private fun updateSetting( newSetting: RealSettingType?, configSetting: MutableStateFlow, mapper: (RealSettingType) -> SettingType, ) { if (newSetting == null) { return } configSetting.value = mapper(newSetting) } @GraphQLIgnore fun updateSettings(settings: Settings) { updateSetting(settings.ip, serverConfig.ip) updateSetting(settings.port, serverConfig.port) // proxy updateSetting(settings.socksProxyEnabled, serverConfig.socksProxyEnabled) updateSetting(settings.socksProxyVersion, serverConfig.socksProxyVersion) updateSetting(settings.socksProxyHost, serverConfig.socksProxyHost) updateSetting(settings.socksProxyPort, serverConfig.socksProxyPort) updateSetting(settings.socksProxyUsername, serverConfig.socksProxyUsername) updateSetting(settings.socksProxyPassword, serverConfig.socksProxyPassword) // webUI updateSetting(settings.webUIFlavor, serverConfig.webUIFlavor) updateSetting(settings.initialOpenInBrowserEnabled, serverConfig.initialOpenInBrowserEnabled) updateSetting(settings.webUIInterface, serverConfig.webUIInterface) updateSetting(settings.electronPath, serverConfig.electronPath) updateSetting(settings.webUIChannel, serverConfig.webUIChannel) updateSetting(settings.webUIUpdateCheckInterval, serverConfig.webUIUpdateCheckInterval) // downloader updateSetting(settings.downloadAsCbz, serverConfig.downloadAsCbz) updateSetting(settings.downloadsPath, serverConfig.downloadsPath) updateSetting(settings.autoDownloadNewChapters, serverConfig.autoDownloadNewChapters) updateSetting(settings.excludeEntryWithUnreadChapters, serverConfig.excludeEntryWithUnreadChapters) updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit) updateSetting(settings.autoDownloadIgnoreReUploads, serverConfig.autoDownloadIgnoreReUploads) updateSetting(settings.downloadConversions, serverConfig.downloadConversions) { list -> list.associate { it.mimeType to ServerConfig.DownloadConversion( target = it.target, compressionLevel = it.compressionLevel, ) } } // extension updateSetting(settings.extensionRepos, serverConfig.extensionRepos) // requests updateSetting(settings.maxSourcesInParallel, serverConfig.maxSourcesInParallel) // updater updateSetting(settings.excludeUnreadChapters, serverConfig.excludeUnreadChapters) updateSetting(settings.excludeNotStarted, serverConfig.excludeNotStarted) updateSetting(settings.excludeCompleted, serverConfig.excludeCompleted) updateSetting(settings.globalUpdateInterval, serverConfig.globalUpdateInterval) updateSetting(settings.updateMangas, serverConfig.updateMangas) // Authentication updateSetting(settings.authMode, serverConfig.authMode) updateSetting(settings.jwtAudience, serverConfig.jwtAudience) updateSetting(settings.jwtTokenExpiry, serverConfig.jwtTokenExpiry) updateSetting(settings.jwtRefreshExpiry, serverConfig.jwtRefreshExpiry) updateSetting(settings.authUsername, serverConfig.authUsername) updateSetting(settings.authPassword, serverConfig.authPassword) updateSetting(settings.basicAuthEnabled, serverConfig.basicAuthEnabled) updateSetting(settings.basicAuthUsername, serverConfig.basicAuthUsername) updateSetting(settings.basicAuthPassword, serverConfig.basicAuthPassword) // misc updateSetting(settings.debugLogsEnabled, serverConfig.debugLogsEnabled) updateSetting(settings.systemTrayEnabled, serverConfig.systemTrayEnabled) updateSetting(settings.maxLogFiles, serverConfig.maxLogFiles) updateSetting(settings.maxLogFileSize, serverConfig.maxLogFileSize) updateSetting(settings.maxLogFolderSize, serverConfig.maxLogFolderSize) // backup updateSetting(settings.backupPath, serverConfig.backupPath) updateSetting(settings.backupTime, serverConfig.backupTime) updateSetting(settings.backupInterval, serverConfig.backupInterval) updateSetting(settings.backupTTL, serverConfig.backupTTL) // local source updateSetting(settings.localSourcePath, serverConfig.localSourcePath) // cloudflare bypass updateSetting(settings.flareSolverrEnabled, serverConfig.flareSolverrEnabled) updateSetting(settings.flareSolverrUrl, serverConfig.flareSolverrUrl) updateSetting(settings.flareSolverrTimeout, serverConfig.flareSolverrTimeout) updateSetting(settings.flareSolverrSessionName, serverConfig.flareSolverrSessionName) updateSetting(settings.flareSolverrSessionTtl, serverConfig.flareSolverrSessionTtl) updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback) // opds updateSetting(settings.opdsUseBinaryFileSizes, serverConfig.opdsUseBinaryFileSizes) updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage) updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress) updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload) updateSetting(settings.opdsShowOnlyUnreadChapters, serverConfig.opdsShowOnlyUnreadChapters) updateSetting(settings.opdsShowOnlyDownloadedChapters, serverConfig.opdsShowOnlyDownloadedChapters) updateSetting(settings.opdsChapterSortOrder, serverConfig.opdsChapterSortOrder) // koreader sync updateSetting(settings.koreaderSyncServerUrl, serverConfig.koreaderSyncServerUrl) updateSetting(settings.koreaderSyncUsername, serverConfig.koreaderSyncUsername) updateSetting(settings.koreaderSyncUserkey, serverConfig.koreaderSyncUserkey) updateSetting(settings.koreaderSyncDeviceId, serverConfig.koreaderSyncDeviceId) updateSetting(settings.koreaderSyncChecksumMethod, serverConfig.koreaderSyncChecksumMethod) updateSetting(settings.koreaderSyncStrategy, serverConfig.koreaderSyncStrategy) updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance) } fun setSettings( dataFetchingEnvironment: DataFetchingEnvironment, input: SetSettingsInput, ): SetSettingsPayload { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, settings) = input validateSettings(settings) updateSettings(settings) return SetSettingsPayload(clientMutationId, SettingsType()) } data class ResetSettingsInput( val clientMutationId: String? = null, ) data class ResetSettingsPayload( val clientMutationId: String?, val settings: SettingsType, ) fun resetSettings( dataFetchingEnvironment: DataFetchingEnvironment, input: ResetSettingsInput, ): ResetSettingsPayload { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId) = input GlobalConfigManager.resetUserConfig() val defaultServerConfig = ServerConfig { GlobalConfigManager.config.getConfig( SERVER_CONFIG_MODULE_NAME, ) } val settings = SettingsType(defaultServerConfig) updateSettings(settings) return ResetSettingsPayload(clientMutationId, settings) } }