diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 2f4bc8b5f..c64cba596 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -5365,6 +5365,7 @@ public class net/mamoe/mirai/utils/BotConfiguration { public static synthetic fun fileBasedDeviceInfo$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/lang/String;ILjava/lang/Object;)V public final fun getAutoReconnectOnForceOffline ()Z public final fun getBotLoggerSupplier ()Lkotlin/jvm/functions/Function1; + public final fun getCacheDirSupplier ()Lkotlin/jvm/functions/Function0; public static final fun getDefault ()Lnet/mamoe/mirai/utils/BotConfiguration; public final fun getDeviceInfo ()Lkotlin/jvm/functions/Function1; public final fun getFirstReconnectDelayMillis ()J @@ -5406,6 +5407,7 @@ public class net/mamoe/mirai/utils/BotConfiguration { public static synthetic fun redirectNetworkLogToFile$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun setAutoReconnectOnForceOffline (Z)V public final fun setBotLoggerSupplier (Lkotlin/jvm/functions/Function1;)V + public final fun setCacheDirSupplier (Lkotlin/jvm/functions/Function0;)V public final fun setDeviceInfo (Lkotlin/jvm/functions/Function1;)V public final fun setFirstReconnectDelayMillis (J)V public final fun setFriendListCache (Lnet/mamoe/mirai/utils/BotConfiguration$FriendListCache;)V diff --git a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt index f9eebb97b..23bf9213a 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt @@ -56,6 +56,11 @@ public open class BotConfiguration { // open for Java */ public var workingDir: File = File(".") + /** + * 缓存数据目录 + */ + public var cacheDirSupplier: (() -> File) = { workingDir.resolve("cache") } + /** * Json 序列化器, 使用 'kotlinx.serialization' */ @@ -396,11 +401,11 @@ public open class BotConfiguration { // open for Java */ public class FriendListCache @JvmOverloads constructor( /** - * 缓存文件位置, 相对于 [workingDir] 的路径. + * 缓存文件位置, 相对于 [cacheDirSupplier] 的路径. * * 注意: 保存的文件仅供内部使用, 将来可能会变化. */ - public val cacheFile: File = File("cache/friends.json"), + public val cacheFile: File = File("friends.json"), /** * 在有好友列表修改时自动保存间隔 */ @@ -432,11 +437,11 @@ public open class BotConfiguration { // open for Java */ public class GroupMemberListCache @JvmOverloads constructor( /** - * 缓存目录位置, 相对于 [workingDir] 的路径. + * 缓存目录位置, 相对于 [cacheDirSupplier] 的路径. * * 注意: 保存的文件仅供内部使用, 将来可能会变化. */ - public val cacheDir: File = File("cache/groups"), + public val cacheDir: File = File("groups"), /** * 在有成员列表修改时自动保存间隔 */ @@ -470,6 +475,7 @@ public open class BotConfiguration { // open for Java return BotConfiguration().also { new -> // To structural order new.workingDir = workingDir + new.cacheDirSupplier = cacheDirSupplier new.json = json new.parentCoroutineContext = parentCoroutineContext new.heartbeatPeriodMillis = heartbeatPeriodMillis diff --git a/mirai-core/src/commonMain/kotlin/AbstractBot.kt b/mirai-core/src/commonMain/kotlin/AbstractBot.kt index d2ae64c2e..c818a7a45 100644 --- a/mirai-core/src/commonMain/kotlin/AbstractBot.kt +++ b/mirai-core/src/commonMain/kotlin/AbstractBot.kt @@ -68,7 +68,7 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor( } // region network - internal val serverList: MutableList<Pair<String, Int>> = DefaultServerList.toMutableList() + internal val serverList: MutableList<Pair<String, Int>> = mutableListOf() val network: N get() = _network @@ -149,7 +149,10 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor( bot.asQQAndroidBot().client.run { if (serverList.isEmpty()) { - serverList.addAll(DefaultServerList) + bot.asQQAndroidBot().bdhSyncer.loadServerListFromCache() + if (serverList.isEmpty()) { + serverList.addAll(DefaultServerList) + } else Unit } else serverList.removeAt(0) } diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index fd1ed3cee..bdf0f15e0 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -25,6 +25,7 @@ import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl import net.mamoe.mirai.internal.contact.uin import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.network.* +import net.mamoe.mirai.internal.network.handler.BdhSessionSyncer import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType @@ -57,6 +58,8 @@ internal class QQAndroidBot constructor( private val account: BotAccount, configuration: BotConfiguration ) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) { + val bdhSyncer: BdhSessionSyncer = BdhSessionSyncer(this) + var client: QQAndroidClient = initClient() private set @@ -79,7 +82,7 @@ internal class QQAndroidBot constructor( val friendListCache: FriendListCache? by lazy { configuration.friendListCache?.cacheFile?.let { cacheFile -> - val ret = configuration.workingDir.resolve(cacheFile).loadAs(FriendListCache.serializer(), JsonForCache) ?: FriendListCache() + val ret = configuration.cacheDirSupplier().resolve(cacheFile).loadAs(FriendListCache.serializer(), JsonForCache) ?: FriendListCache() @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") bot.eventChannel.parentScope(this@QQAndroidBot) @@ -130,6 +133,7 @@ internal class QQAndroidBot constructor( @ThisApiMustBeUsedInWithConnectionLockBlock @Throws(LoginFailedException::class) // only override suspend fun relogin(cause: Throwable?) { + bdhSyncer.loadFromCache() client.useNextServers { host, port -> network.closeEverythingAndRelogin(host, port, cause, 0) } diff --git a/mirai-core/src/commonMain/kotlin/network/ContactListCache.kt b/mirai-core/src/commonMain/kotlin/network/ContactListCache.kt index f5fde74c8..5d79c4920 100644 --- a/mirai-core/src/commonMain/kotlin/network/ContactListCache.kt +++ b/mirai-core/src/commonMain/kotlin/network/ContactListCache.kt @@ -84,7 +84,7 @@ internal class GroupMemberListCaches( } private val cacheDir by lazy { - bot.configuration.groupMemberListCache!!.cacheDir.let { bot.configuration.workingDir.resolve(it) } + bot.configuration.groupMemberListCache!!.cacheDir.let { bot.configuration.cacheDirSupplier().resolve(it) } } private fun resolveCacheFile(groupCode: Long): File { diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index bce6b8407..b7473f040 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -19,6 +19,7 @@ import kotlinx.io.core.BytePacketBuilder import kotlinx.io.core.String import kotlinx.io.core.toByteArray import kotlinx.io.core.writeFully +import kotlinx.serialization.Serializable import net.mamoe.mirai.data.OnlineStatus import net.mamoe.mirai.internal.BotAccount import net.mamoe.mirai.internal.QQAndroidBot @@ -281,11 +282,6 @@ internal open class QQAndroidClient( lateinit var t104: ByteArray - /** - * from ConfigPush.PushReq - */ - @JvmField - val bdhSession: CompletableDeferred<BdhSession> = CompletableDeferred() } internal fun BytePacketBuilder.writeLoginExtraData(loginExtraData: LoginExtraData) { @@ -298,6 +294,7 @@ internal fun BytePacketBuilder.writeLoginExtraData(loginExtraData: LoginExtraDat } } +@Serializable internal class BdhSession( val sigSession: ByteArray, val sessionKey: ByteArray, diff --git a/mirai-core/src/commonMain/kotlin/network/handler/BdhSessionSyncer.kt b/mirai-core/src/commonMain/kotlin/network/handler/BdhSessionSyncer.kt new file mode 100644 index 000000000..28c22b4f3 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/handler/BdhSessionSyncer.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.internal.network.handler + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.PairSerializer +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.network.BdhSession +import net.mamoe.mirai.internal.network.JsonForCache +import java.io.File + +private val ServerListSerializer: KSerializer<List<Pair<String, Int>>> = + ListSerializer(PairSerializer(String.serializer(), Int.serializer())) + +@OptIn(ExperimentalCoroutinesApi::class) +internal class BdhSessionSyncer( + private val bot: QQAndroidBot +) { + var bdhSession: CompletableDeferred<BdhSession> = CompletableDeferred() + val hasSession: Boolean get() = bdhSession.isCompleted + + fun overrideSession( + session: BdhSession, + doSave: Boolean = true + ) { + bdhSession.complete(session) + bdhSession = CompletableDeferred(session) + if (doSave) { + saveToCache() + } + } + + private val sessionCacheFile: File + get() = bot.configuration.cacheDirSupplier().resolve("session.json") + private val serverListCacheFile: File + get() = bot.configuration.cacheDirSupplier().resolve("serverlist.json") + + fun loadServerListFromCache() { + val serverListCacheFile = this.serverListCacheFile + if (serverListCacheFile.isFile) { + bot.network.logger.verbose("Loading server list from cache.") + kotlin.runCatching { + val list = JsonForCache.decodeFromString(ServerListSerializer, serverListCacheFile.readText()) + bot.serverList.clear() + bot.serverList.addAll(list) + }.onFailure { + bot.network.logger.warning("Error in loading server list from cache", it) + } + } else { + bot.network.logger.verbose("No server list cached.") + } + } + + fun loadFromCache() { + val sessionCacheFile = this.sessionCacheFile + if (sessionCacheFile.isFile) { + bot.network.logger.verbose("Loading BdhSession from cache file") + kotlin.runCatching { + overrideSession( + JsonForCache.decodeFromString(BdhSession.serializer(), sessionCacheFile.readText()), + doSave = false + ) + }.onFailure { + bot.network.logger.warning("Error in loading BdhSession from cache", it) + } + } else { + bot.network.logger.verbose("No BdhSession cache") + } + } + + fun saveServerListToCache() { + val serverListCacheFile = this.serverListCacheFile + serverListCacheFile.parentFile?.mkdirs() + + bot.network.logger.verbose("Saving server list to cache") + kotlin.runCatching { + serverListCacheFile.writeText( + JsonForCache.encodeToString( + ServerListSerializer, + bot.serverList + ) + ) + }.onFailure { + bot.network.logger.warning("Error in saving ServerList to cache.", it) + } + } + + fun saveToCache() { + val sessionCacheFile = this.sessionCacheFile + sessionCacheFile.parentFile?.mkdirs() + if (bdhSession.isCompleted) { + bot.network.logger.verbose("Saving bdh session to cache") + kotlin.runCatching { + sessionCacheFile.writeText( + JsonForCache.encodeToString( + BdhSession.serializer(), + bdhSession.getCompleted() + ) + ) + }.onFailure { + bot.network.logger.warning("Error in saving BdhSession to cache.", it) + } + } else { + sessionCacheFile.delete() + bot.network.logger.verbose("No BdhSession to save to cache") + } + + } +} diff --git a/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt index fee835efa..8d40ffd75 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/QQAndroidBotNetworkHandler.kt @@ -359,24 +359,20 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo logger.info { "Awaiting ConfigPushSvc.PushReq." } when (val resp: ConfigPushSvc.PushReq.PushReqResponse? = nextEventOrNull(20_000)) { null -> { - kotlin.runCatching { bot.client.bdhSession.completeExceptionally(CancellationException("Timeout waiting for ConfigPushSvc.PushReq")) } - logger.warning { "Missing ConfigPushSvc.PushReq. File uploading may be affected." } + val hasSession = bot.bdhSyncer.hasSession + kotlin.runCatching { bot.bdhSyncer.bdhSession.completeExceptionally(CancellationException("Timeout waiting for ConfigPushSvc.PushReq")) } + if (!hasSession) { + logger.warning { "Missing ConfigPushSvc.PushReq. File uploading may be affected." } + } else { + logger.warning { "Missing ConfigPushSvc.PushReq. Using latest response. File uploading may be affected." } + } } is ConfigPushSvc.PushReq.PushReqResponse.Success -> { logger.info { "ConfigPushSvc.PushReq: Success." } } is ConfigPushSvc.PushReq.PushReqResponse.ChangeServer -> { - bot.logger.info { "Server requires reconnect." } - bot.logger.info { "Server list: ${resp.serverList.joinToString()}." } - - if (resp.serverList.isNotEmpty()) { - bot.serverList.clear() - resp.serverList.shuffled().forEach { - bot.serverList.add(it.host to it.port) - } - } - - bot.launch { BotOfflineEvent.RequireReconnect(bot).broadcast() } + logger.info { "ConfigPushSvc.PushReq: Require reconnect" } + // handled in ConfigPushSvc return@launch } } diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index a7499c6ca..8b972759b 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -59,7 +59,7 @@ internal object Highway { fallbackSession: (Throwable) -> BdhSession = { throw IllegalStateException("Failed to get bdh session", it) } ): BdhUploadResponse { val bdhSession = kotlin.runCatching { - val deferred = bot.client.bdhSession + val deferred = bot.bdhSyncer.bdhSession // no need to care about timeout. proceed by bot init @OptIn(ExperimentalCoroutinesApi::class) if (noBdhAwait) deferred.getCompleted() else deferred.await() diff --git a/mirai-core/src/commonMain/kotlin/network/keys.kt b/mirai-core/src/commonMain/kotlin/network/keys.kt index 415d1092a..76159b262 100644 --- a/mirai-core/src/commonMain/kotlin/network/keys.kt +++ b/mirai-core/src/commonMain/kotlin/network/keys.kt @@ -200,7 +200,10 @@ internal open class KeyWithCreationTime( internal suspend inline fun QQAndroidClient.useNextServers(crossinline block: suspend (host: String, port: Int) -> Unit) { if (bot.serverList.isEmpty()) { - bot.serverList.addAll(DefaultServerList) + bot.bdhSyncer.loadServerListFromCache() + if (bot.serverList.isEmpty()) { + bot.serverList.addAll(DefaultServerList) + } } retryCatchingExceptions(bot.serverList.size, except = LoginFailedException::class) l@{ val pair = bot.serverList[0] diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt index 591c42bce..fe9aaeb90 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt @@ -9,10 +9,14 @@ package net.mamoe.mirai.internal.network.protocol.packet.login +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.io.core.ByteReadPacket import kotlinx.serialization.Serializable import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.event.Event +import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.network.BdhSession @@ -30,6 +34,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readUniPacket import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId import net.mamoe.mirai.internal.utils.io.serialization.writeJceStruct +import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.toUHexString import java.lang.IllegalStateException import net.mamoe.mirai.internal.network.protocol.data.jce.PushReq as PushReqJceStruct @@ -110,7 +115,7 @@ internal class ConfigPushSvc { val bigDataChannel = fileStoragePushFSSvcList.bigDataChannel if (bigDataChannel?.vBigdataPbBuf == null) { - client.bdhSession.completeExceptionally(IllegalStateException("BdhSession not received.")) + bot.bdhSyncer.bdhSession.completeExceptionally(IllegalStateException("BdhSession not received.")) return } @@ -134,16 +139,35 @@ internal class ConfigPushSvc { session }.fold( onSuccess = { - client.bdhSession.complete(it) + bdhSyncer.overrideSession(it) }, onFailure = { cause -> val e = IllegalStateException("Failed to decode BdhSession", cause) - client.bdhSession.completeExceptionally(e) + bdhSyncer.bdhSession.completeExceptionally(e) logger.error(e) } ) } + fun handleRequireReconnect(resp: PushReqResponse.ChangeServer) { + bot.logger.info { "Server requires reconnect." } + bot.logger.info { "Server list: ${resp.serverList.joinToString()}." } + + if (resp.serverList.isNotEmpty()) { + bot.serverList.clear() + resp.serverList.shuffled().forEach { + bot.serverList.add(it.host to it.port) + } + } + bot.bdhSyncer.saveToCache() + bot.bdhSyncer.saveServerListToCache() + + bot.launch { + delay(1000) + BotOfflineEvent.RequireReconnect(bot).broadcast() + } + } + when (packet) { is PushReqResponse.Success -> { handleSuccess(packet) @@ -172,6 +196,10 @@ internal class ConfigPushSvc { // writePacket(this.build().debugPrintThis()) } } + is PushReqResponse.ChangeServer -> { + handleRequireReconnect(packet) + return null + } else -> { // handled in QQABot return null