Fix/remove koreader-sync credentials from server config (#1758)

* Remove koreader-sync credentials from config

These are supposed to be set via the login/logout mutations and are not meant to be set manually by the user. Thus, they are not really settings and do not belong to the config

* Reduce log levels of KoreaderSyncService
This commit is contained in:
schroda
2025-11-01 19:31:07 +01:00
committed by GitHub
parent 53c4659044
commit 4dbd9d70d2
5 changed files with 100 additions and 47 deletions

View File

@@ -35,11 +35,15 @@ 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"))
}

View File

@@ -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<String> 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<String> 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<String> 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<KoreaderSyncChecksumMethod> by EnumSetting(

View File

@@ -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<KoSyncConnectPayload> =
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),
)
}

View File

@@ -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,
)

View File

@@ -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<Application>().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<String, String> {
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)