diff --git a/server/server-config/build.gradle.kts b/server/server-config/build.gradle.kts index bbd67dff..d0e33e0e 100644 --- a/server/server-config/build.gradle.kts +++ b/server/server-config/build.gradle.kts @@ -34,12 +34,16 @@ dependencies { // GraphQL types used in ServerConfig implementation(libs.graphql.kotlin.scheme) - + + // Dependency Injection + implementation(libs.injekt) + // AndroidCompat for SystemPropertyOverridableConfigModule implementation(projects.androidCompat.config) // Serialization implementation(libs.serialization.json) implementation(libs.serialization.protobuf) + implementation(project(":AndroidCompat")) } diff --git a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index 8223e12d..cd226f1c 100644 --- a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -7,6 +7,8 @@ package suwayomi.tachidesk.server * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import android.app.Application +import android.content.Context import com.typesafe.config.Config import io.github.config4k.toConfig import kotlinx.coroutines.CoroutineScope @@ -55,11 +57,13 @@ import suwayomi.tachidesk.server.settings.StringSetting import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import kotlin.collections.associate +import kotlin.getValue import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import uy.kohesive.injekt.injectLazy val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -67,6 +71,8 @@ const val SERVER_CONFIG_MODULE_NAME = "server" val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } +private val application: Application by injectLazy() + // Settings are ordered by "protoNumber". class ServerConfig( getConfig: () -> Config, @@ -604,24 +610,57 @@ class ServerConfig( description = "KOReader Sync Server URL. Public alternative: https://kosync.ak-team.com:3042/", ) + @Deprecated("Moved to preference store. User is supposed to use a login/logout mutation") val koreaderSyncUsername: MutableStateFlow by StringSetting( protoNumber = 60, group = SettingGroup.KOREADER_SYNC, defaultValue = "", excludeFromBackup = true, + deprecated = SettingsRegistry.SettingDeprecated( + replaceWith = "MOVE TO PREFERENCES", + message = "Moved to preference store. User is supposed to use a login/logout mutation", + migrateConfig = { value, config -> + val koreaderPreferences = application.getSharedPreferences("koreader_sync", Context.MODE_PRIVATE) + koreaderPreferences.edit().putString("username", value.unwrapped() as? String).apply() + + config + } + ), ) + @Deprecated("Moved to preference store. User is supposed to use a login/logout mutation") val koreaderSyncUserkey: MutableStateFlow by StringSetting( protoNumber = 61, group = SettingGroup.KOREADER_SYNC, defaultValue = "", excludeFromBackup = true, + deprecated = SettingsRegistry.SettingDeprecated( + replaceWith = "MOVE TO PREFERENCES", + message = "Moved to preference store. User is supposed to use a login/logout mutation", + migrateConfig = { value, config -> + val koreaderPreferences = application.getSharedPreferences("koreader_sync", Context.MODE_PRIVATE) + koreaderPreferences.edit().putString("user_key", value.unwrapped() as? String).apply() + + config + } + ), ) + @Deprecated("Moved to preference store. Is supposed to be random and gets auto generated") val koreaderSyncDeviceId: MutableStateFlow by StringSetting( protoNumber = 62, group = SettingGroup.KOREADER_SYNC, defaultValue = "", + deprecated = SettingsRegistry.SettingDeprecated( + replaceWith = "MOVE TO PREFERENCES", + message = "Moved to preference store. Is supposed to be random and gets auto generated", + migrateConfig = { value, config -> + val koreaderPreferences = application.getSharedPreferences("koreader_sync", Context.MODE_PRIVATE) + koreaderPreferences.edit().putString("device_id", value.unwrapped() as? String).apply() + + config + } + ), ) val koreaderSyncChecksumMethod: MutableStateFlow by EnumSetting( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt index 42d38424..b8ceae3a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt @@ -8,8 +8,8 @@ import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload +import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload -import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.graphql.types.SyncConflictInfoType import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.model.table.ChapterTable @@ -26,14 +26,12 @@ class KoreaderSyncMutation { @RequireAuth fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture = future { - val result = KoreaderSyncService.connect(input.username, input.password) + val (message, status) = KoreaderSyncService.connect(input.username, input.password) KoSyncConnectPayload( clientMutationId = input.clientMutationId, - success = result.success, - message = result.message, - username = result.username, - settings = SettingsType(), + message = message, + status = status, ) } @@ -47,8 +45,7 @@ class KoreaderSyncMutation { KoreaderSyncService.logout() LogoutKoSyncAccountPayload( clientMutationId = input.clientMutationId, - success = true, - settings = SettingsType(), + status = KoSyncStatusPayload(isLoggedIn = false, username = null), ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/KoreaderSyncPayloads.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/KoreaderSyncPayloads.kt index e74535f7..98bd41e6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/KoreaderSyncPayloads.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/KoreaderSyncPayloads.kt @@ -7,14 +7,11 @@ data class KoSyncStatusPayload( data class KoSyncConnectPayload( val clientMutationId: String?, - val success: Boolean, + val status: KoSyncStatusPayload, val message: String?, - val username: String?, - val settings: SettingsType, ) data class LogoutKoSyncAccountPayload( val clientMutationId: String?, - val success: Boolean, - val settings: SettingsType, + val status: KoSyncStatusPayload, ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt index 96032681..1194ed56 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/sync/KoreaderSyncService.kt @@ -1,5 +1,7 @@ package suwayomi.tachidesk.manga.impl.sync +import android.app.Application +import android.content.Context import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.util.lang.Hash @@ -22,12 +24,20 @@ import suwayomi.tachidesk.manga.impl.util.KoreaderHelper import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.server.serverConfig +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.util.UUID import kotlin.math.abs object KoreaderSyncService { + private val preferences = Injekt.get().getSharedPreferences("koreader_sync", Context.MODE_PRIVATE) + + private const val USERNAME_KEY = "username" + private const val USERKEY_KEY = "user_key" + private const val DEVICE_ID_KEY = "client_id" + private val logger = KotlinLogging.logger {} private val network: NetworkHelper by injectLazy() private val json: Json by injectLazy() @@ -62,9 +72,8 @@ object KoreaderSyncService { ) data class ConnectResult( - val success: Boolean, val message: String? = null, - val username: String? = null, + val status: KoSyncStatusPayload, ) private data class AuthResult( @@ -86,7 +95,8 @@ object KoreaderSyncService { .build() private suspend fun getOrGenerateDeviceId(): String { - var deviceId = serverConfig.koreaderSyncDeviceId.value + var deviceId = preferences.getString(DEVICE_ID_KEY, "")!! + if (deviceId.isBlank()) { deviceId = UUID @@ -95,7 +105,7 @@ object KoreaderSyncService { .replace("-", "") .uppercase() logger.info { "[KOSYNC] Generated new KOSync Device ID: $deviceId" } - serverConfig.koreaderSyncDeviceId.value = deviceId + preferences.edit().putString(DEVICE_ID_KEY, deviceId).apply() } return deviceId } @@ -119,7 +129,7 @@ object KoreaderSyncService { val newHash = when (checksumMethod) { KoreaderSyncChecksumMethod.BINARY -> { - logger.info { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." } + logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from downloaded content." } try { // Always create a CBZ in memory if it doesn't exist val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId) @@ -141,7 +151,7 @@ object KoreaderSyncService { } } KoreaderSyncChecksumMethod.FILENAME -> { - logger.info { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from filename." } + logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from filename." } (ChapterTable innerJoin MangaTable) .select(ChapterTable.name, MangaTable.title) .where { ChapterTable.id eq chapterId } @@ -230,6 +240,24 @@ object KoreaderSyncService { } } + private fun getCredentials(): Pair { + val username = preferences.getString(USERNAME_KEY, "")!! + val userkey = preferences.getString(USERKEY_KEY, "")!! + + return Pair(username, userkey) + } + + private fun setCredentials( + username: String, + userkey: String, + ) { + preferences + .edit() + .putString(USERNAME_KEY, username) + .putString(USERKEY_KEY, userkey) + .apply() + } + suspend fun connect( username: String, password: String, @@ -238,34 +266,30 @@ object KoreaderSyncService { val authResult = authorize(username, userkey) if (authResult.success) { - serverConfig.koreaderSyncUsername.value = username - serverConfig.koreaderSyncUserkey.value = userkey - return ConnectResult(true, "Login successful.", username) + setCredentials(username, userkey) + return ConnectResult("Login successful.", KoSyncStatusPayload(isLoggedIn = true, username = username)) } if (authResult.isUserNotFoundError) { logger.info { "[KOSYNC CONNECT] Authorization failed, attempting to register new user." } val registerResult = register(username, userkey) return if (registerResult.success) { - serverConfig.koreaderSyncUsername.value = username - serverConfig.koreaderSyncUserkey.value = userkey - ConnectResult(true, "Registration successful.", username) + setCredentials(username, userkey) + ConnectResult("Registration successful.", KoSyncStatusPayload(isLoggedIn = true, username = username)) } else { - ConnectResult(false, registerResult.message ?: "Registration failed.", null) + ConnectResult(registerResult.message ?: "Registration failed.", KoSyncStatusPayload(isLoggedIn = false, username = null)) } } - return ConnectResult(false, authResult.message ?: "Authentication failed.", null) + return ConnectResult(authResult.message ?: "Authentication failed.", KoSyncStatusPayload(isLoggedIn = false, username = null)) } suspend fun logout() { - serverConfig.koreaderSyncUsername.value = "" - serverConfig.koreaderSyncUserkey.value = "" + setCredentials("", "") } suspend fun getStatus(): KoSyncStatusPayload { - val username = serverConfig.koreaderSyncUsername.value - val userkey = serverConfig.koreaderSyncUserkey.value + val (username, userkey) = getCredentials() if (username.isBlank() || userkey.isBlank()) { return KoSyncStatusPayload(isLoggedIn = false, username = null) } @@ -284,12 +308,9 @@ object KoreaderSyncService { return } - val username = serverConfig.koreaderSyncUsername.value - val userkey = serverConfig.koreaderSyncUserkey.value + val (username, userkey) = getCredentials() if (username.isBlank() || userkey.isBlank()) return - logger.info { "[KOSYNC PUSH] Init." } - val chapterHash = getOrGenerateChapterHash(chapterId) if (chapterHash.isNullOrBlank()) { logger.info { "[KOSYNC PUSH] Aborted for chapterId=$chapterId: No hash." } @@ -334,13 +355,11 @@ object KoreaderSyncService { addHeader("x-auth-key", userkey) } - logger.info { "[KOSYNC PUSH] PUT request to URL: ${request.url}" } - logger.info { "[KOSYNC PUSH] Sending data: $requestBody" } + logger.info { "[KOSYNC PUSH] url= ${request.url} - Sending data: $requestBody" } network.client.newCall(request).await().use { response -> val responseBody = response.body.string() - logger.info { "[KOSYNC PUSH] PUT response status: ${response.code}" } - logger.info { "[KOSYNC PUSH] PUT response body: $responseBody" } + logger.debug { "[KOSYNC PUSH] PUT response status: ${response.code}; response body: $responseBody" } if (!response.isSuccessful) { logger.warn { "[KOSYNC PUSH] Failed for chapterId=$chapterId: ${response.code}" } } else { @@ -363,13 +382,12 @@ object KoreaderSyncService { return null } - val username = serverConfig.koreaderSyncUsername.value - val userkey = serverConfig.koreaderSyncUserkey.value + val (username, userkey) = getCredentials() if (username.isBlank() || userkey.isBlank()) return null val chapterHash = getOrGenerateChapterHash(chapterId) if (chapterHash.isNullOrBlank()) { - logger.info { "[KOSYNC PULL] Aborted for chapterId=$chapterId: No hash." } + logger.debug { "[KOSYNC PULL] Aborted for chapterId=$chapterId: No hash." } return null } @@ -380,14 +398,12 @@ object KoreaderSyncService { addHeader("x-auth-user", username) addHeader("x-auth-key", userkey) } - logger.info { "[KOSYNC PULL] GET request to URL: ${request.url}" } - network.client.newCall(request).await().use { response -> - logger.info { "[KOSYNC PULL] GET response status: ${response.code}" } + logger.debug { "[KOSYNC PULL] GET response status: ${response.code}" } if (response.isSuccessful) { val body = response.body.string() - logger.info { "[KOSYNC PULL] GET response body: $body" } + logger.debug { "[KOSYNC PULL] GET response body: $body" } if (body.isBlank() || body == "{}") return null val progressResponse = json.decodeFromString(KoreaderProgressResponse.serializer(), body)