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

@@ -34,12 +34,16 @@ dependencies {
// GraphQL types used in ServerConfig // GraphQL types used in ServerConfig
implementation(libs.graphql.kotlin.scheme) implementation(libs.graphql.kotlin.scheme)
// Dependency Injection
implementation(libs.injekt)
// AndroidCompat for SystemPropertyOverridableConfigModule // AndroidCompat for SystemPropertyOverridableConfigModule
implementation(projects.androidCompat.config) implementation(projects.androidCompat.config)
// Serialization // Serialization
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.serialization.protobuf) 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 * 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/. */ * 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 com.typesafe.config.Config
import io.github.config4k.toConfig import io.github.config4k.toConfig
import kotlinx.coroutines.CoroutineScope 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.GlobalConfigManager
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
import kotlin.collections.associate import kotlin.collections.associate
import kotlin.getValue
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import uy.kohesive.injekt.injectLazy
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -67,6 +71,8 @@ const val SERVER_CONFIG_MODULE_NAME = "server"
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
private val application: Application by injectLazy()
// Settings are ordered by "protoNumber". // Settings are ordered by "protoNumber".
class ServerConfig( class ServerConfig(
getConfig: () -> Config, getConfig: () -> Config,
@@ -604,24 +610,57 @@ class ServerConfig(
description = "KOReader Sync Server URL. Public alternative: https://kosync.ak-team.com:3042/", 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( val koreaderSyncUsername: MutableStateFlow<String> by StringSetting(
protoNumber = 60, protoNumber = 60,
group = SettingGroup.KOREADER_SYNC, group = SettingGroup.KOREADER_SYNC,
defaultValue = "", defaultValue = "",
excludeFromBackup = true, 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( val koreaderSyncUserkey: MutableStateFlow<String> by StringSetting(
protoNumber = 61, protoNumber = 61,
group = SettingGroup.KOREADER_SYNC, group = SettingGroup.KOREADER_SYNC,
defaultValue = "", defaultValue = "",
excludeFromBackup = true, 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( val koreaderSyncDeviceId: MutableStateFlow<String> by StringSetting(
protoNumber = 62, protoNumber = 62,
group = SettingGroup.KOREADER_SYNC, group = SettingGroup.KOREADER_SYNC,
defaultValue = "", 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( 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.directives.RequireAuth
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload
import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload
import suwayomi.tachidesk.graphql.types.SettingsType
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
@@ -26,14 +26,12 @@ class KoreaderSyncMutation {
@RequireAuth @RequireAuth
fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> = fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> =
future { future {
val result = KoreaderSyncService.connect(input.username, input.password) val (message, status) = KoreaderSyncService.connect(input.username, input.password)
KoSyncConnectPayload( KoSyncConnectPayload(
clientMutationId = input.clientMutationId, clientMutationId = input.clientMutationId,
success = result.success, message = message,
message = result.message, status = status,
username = result.username,
settings = SettingsType(),
) )
} }
@@ -47,8 +45,7 @@ class KoreaderSyncMutation {
KoreaderSyncService.logout() KoreaderSyncService.logout()
LogoutKoSyncAccountPayload( LogoutKoSyncAccountPayload(
clientMutationId = input.clientMutationId, clientMutationId = input.clientMutationId,
success = true, status = KoSyncStatusPayload(isLoggedIn = false, username = null),
settings = SettingsType(),
) )
} }

View File

@@ -7,14 +7,11 @@ data class KoSyncStatusPayload(
data class KoSyncConnectPayload( data class KoSyncConnectPayload(
val clientMutationId: String?, val clientMutationId: String?,
val success: Boolean, val status: KoSyncStatusPayload,
val message: String?, val message: String?,
val username: String?,
val settings: SettingsType,
) )
data class LogoutKoSyncAccountPayload( data class LogoutKoSyncAccountPayload(
val clientMutationId: String?, val clientMutationId: String?,
val success: Boolean, val status: KoSyncStatusPayload,
val settings: SettingsType,
) )

View File

@@ -1,5 +1,7 @@
package suwayomi.tachidesk.manga.impl.sync 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.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.lang.Hash 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.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import kotlin.math.abs import kotlin.math.abs
object KoreaderSyncService { 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 logger = KotlinLogging.logger {}
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy() private val json: Json by injectLazy()
@@ -62,9 +72,8 @@ object KoreaderSyncService {
) )
data class ConnectResult( data class ConnectResult(
val success: Boolean,
val message: String? = null, val message: String? = null,
val username: String? = null, val status: KoSyncStatusPayload,
) )
private data class AuthResult( private data class AuthResult(
@@ -86,7 +95,8 @@ object KoreaderSyncService {
.build() .build()
private suspend fun getOrGenerateDeviceId(): String { private suspend fun getOrGenerateDeviceId(): String {
var deviceId = serverConfig.koreaderSyncDeviceId.value var deviceId = preferences.getString(DEVICE_ID_KEY, "")!!
if (deviceId.isBlank()) { if (deviceId.isBlank()) {
deviceId = deviceId =
UUID UUID
@@ -95,7 +105,7 @@ object KoreaderSyncService {
.replace("-", "") .replace("-", "")
.uppercase() .uppercase()
logger.info { "[KOSYNC] Generated new KOSync Device ID: $deviceId" } logger.info { "[KOSYNC] Generated new KOSync Device ID: $deviceId" }
serverConfig.koreaderSyncDeviceId.value = deviceId preferences.edit().putString(DEVICE_ID_KEY, deviceId).apply()
} }
return deviceId return deviceId
} }
@@ -119,7 +129,7 @@ object KoreaderSyncService {
val newHash = val newHash =
when (checksumMethod) { when (checksumMethod) {
KoreaderSyncChecksumMethod.BINARY -> { 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 { try {
// Always create a CBZ in memory if it doesn't exist // Always create a CBZ in memory if it doesn't exist
val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId) val (stream, _) = ChapterDownloadHelper.getArchiveStreamWithSize(mangaId, chapterId)
@@ -141,7 +151,7 @@ object KoreaderSyncService {
} }
} }
KoreaderSyncChecksumMethod.FILENAME -> { 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) (ChapterTable innerJoin MangaTable)
.select(ChapterTable.name, MangaTable.title) .select(ChapterTable.name, MangaTable.title)
.where { ChapterTable.id eq chapterId } .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( suspend fun connect(
username: String, username: String,
password: String, password: String,
@@ -238,34 +266,30 @@ object KoreaderSyncService {
val authResult = authorize(username, userkey) val authResult = authorize(username, userkey)
if (authResult.success) { if (authResult.success) {
serverConfig.koreaderSyncUsername.value = username setCredentials(username, userkey)
serverConfig.koreaderSyncUserkey.value = userkey return ConnectResult("Login successful.", KoSyncStatusPayload(isLoggedIn = true, username = username))
return ConnectResult(true, "Login successful.", username)
} }
if (authResult.isUserNotFoundError) { if (authResult.isUserNotFoundError) {
logger.info { "[KOSYNC CONNECT] Authorization failed, attempting to register new user." } logger.info { "[KOSYNC CONNECT] Authorization failed, attempting to register new user." }
val registerResult = register(username, userkey) val registerResult = register(username, userkey)
return if (registerResult.success) { return if (registerResult.success) {
serverConfig.koreaderSyncUsername.value = username setCredentials(username, userkey)
serverConfig.koreaderSyncUserkey.value = userkey ConnectResult("Registration successful.", KoSyncStatusPayload(isLoggedIn = true, username = username))
ConnectResult(true, "Registration successful.", username)
} else { } 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() { suspend fun logout() {
serverConfig.koreaderSyncUsername.value = "" setCredentials("", "")
serverConfig.koreaderSyncUserkey.value = ""
} }
suspend fun getStatus(): KoSyncStatusPayload { suspend fun getStatus(): KoSyncStatusPayload {
val username = serverConfig.koreaderSyncUsername.value val (username, userkey) = getCredentials()
val userkey = serverConfig.koreaderSyncUserkey.value
if (username.isBlank() || userkey.isBlank()) { if (username.isBlank() || userkey.isBlank()) {
return KoSyncStatusPayload(isLoggedIn = false, username = null) return KoSyncStatusPayload(isLoggedIn = false, username = null)
} }
@@ -284,12 +308,9 @@ object KoreaderSyncService {
return return
} }
val username = serverConfig.koreaderSyncUsername.value val (username, userkey) = getCredentials()
val userkey = serverConfig.koreaderSyncUserkey.value
if (username.isBlank() || userkey.isBlank()) return if (username.isBlank() || userkey.isBlank()) return
logger.info { "[KOSYNC PUSH] Init." }
val chapterHash = getOrGenerateChapterHash(chapterId) val chapterHash = getOrGenerateChapterHash(chapterId)
if (chapterHash.isNullOrBlank()) { if (chapterHash.isNullOrBlank()) {
logger.info { "[KOSYNC PUSH] Aborted for chapterId=$chapterId: No hash." } logger.info { "[KOSYNC PUSH] Aborted for chapterId=$chapterId: No hash." }
@@ -334,13 +355,11 @@ object KoreaderSyncService {
addHeader("x-auth-key", userkey) addHeader("x-auth-key", userkey)
} }
logger.info { "[KOSYNC PUSH] PUT request to URL: ${request.url}" } logger.info { "[KOSYNC PUSH] url= ${request.url} - Sending data: $requestBody" }
logger.info { "[KOSYNC PUSH] Sending data: $requestBody" }
network.client.newCall(request).await().use { response -> network.client.newCall(request).await().use { response ->
val responseBody = response.body.string() val responseBody = response.body.string()
logger.info { "[KOSYNC PUSH] PUT response status: ${response.code}" } logger.debug { "[KOSYNC PUSH] PUT response status: ${response.code}; response body: $responseBody" }
logger.info { "[KOSYNC PUSH] PUT response body: $responseBody" }
if (!response.isSuccessful) { if (!response.isSuccessful) {
logger.warn { "[KOSYNC PUSH] Failed for chapterId=$chapterId: ${response.code}" } logger.warn { "[KOSYNC PUSH] Failed for chapterId=$chapterId: ${response.code}" }
} else { } else {
@@ -363,13 +382,12 @@ object KoreaderSyncService {
return null return null
} }
val username = serverConfig.koreaderSyncUsername.value val (username, userkey) = getCredentials()
val userkey = serverConfig.koreaderSyncUserkey.value
if (username.isBlank() || userkey.isBlank()) return null if (username.isBlank() || userkey.isBlank()) return null
val chapterHash = getOrGenerateChapterHash(chapterId) val chapterHash = getOrGenerateChapterHash(chapterId)
if (chapterHash.isNullOrBlank()) { 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 return null
} }
@@ -380,14 +398,12 @@ object KoreaderSyncService {
addHeader("x-auth-user", username) addHeader("x-auth-user", username)
addHeader("x-auth-key", userkey) addHeader("x-auth-key", userkey)
} }
logger.info { "[KOSYNC PULL] GET request to URL: ${request.url}" }
network.client.newCall(request).await().use { response -> 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) { if (response.isSuccessful) {
val body = response.body.string() 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 if (body.isBlank() || body == "{}") return null
val progressResponse = json.decodeFromString(KoreaderProgressResponse.serializer(), body) val progressResponse = json.decodeFromString(KoreaderProgressResponse.serializer(), body)