Him188 преди 6 години
родител
ревизия
a00989668e

+ 6 - 0
UpdateLog.md

@@ -0,0 +1,6 @@
+# UpdateLog
+
+## Main version 0
+
+### 0.3.0
+- 更新

+ 1 - 1
gradle.properties

@@ -1,7 +1,7 @@
 # style guide
 kotlin.code.style=official
 # config
-mirai_version=0.3.0
+mirai_version=0.5.0
 kotlin.incremental.multiplatform=true
 kotlin.parallel.tasks.in.project=true
 # kotlin

+ 45 - 12
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt

@@ -12,11 +12,13 @@ import net.mamoe.mirai.contact.internal.QQImpl
 import net.mamoe.mirai.network.BotNetworkHandler
 import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler
 import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
+import net.mamoe.mirai.network.protocol.tim.packet.login.isSuccess
 import net.mamoe.mirai.utils.BotConfiguration
 import net.mamoe.mirai.utils.DefaultLogger
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
 import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
 import kotlin.jvm.JvmOverloads
 
 data class BotAccount(
@@ -24,6 +26,15 @@ data class BotAccount(
     val password: String//todo 不保存 password?
 )
 
+@Suppress("FunctionName")
+suspend inline fun Bot(account: BotAccount, logger: MiraiLogger): Bot = Bot(account, logger, coroutineContext)
+
+@Suppress("FunctionName")
+suspend inline fun Bot(account: BotAccount): Bot = Bot(account, coroutineContext)
+
+@Suppress("FunctionName")
+suspend inline fun Bot(qq: UInt, password: String): Bot = Bot(qq, password, coroutineContext)
+
 /**
  * Mirai 的机器人. 一个机器人实例登录一个 QQ 账号.
  * Mirai 为多账号设计, 可同时维护多个机器人.
@@ -54,15 +65,16 @@ data class BotAccount(
  * @author NaturalHG
  * @see Contact
  */
-class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
-    override val coroutineContext: CoroutineContext = SupervisorJob()
+class Bot(val account: BotAccount, val logger: MiraiLogger, context: CoroutineContext) : CoroutineScope {
+    private val supervisorJob = SupervisorJob(context[Job])
+    override val coroutineContext: CoroutineContext = context + supervisorJob
 
-    constructor(qq: UInt, password: String) : this(BotAccount(qq, password))
-    constructor(account: BotAccount) : this(account, DefaultLogger("Bot(" + account.id + ")"))
+    constructor(qq: UInt, password: String, context: CoroutineContext) : this(BotAccount(qq, password), context)
+    constructor(account: BotAccount, context: CoroutineContext) : this(account, DefaultLogger("Bot(" + account.id + ")"), context)
 
     val contacts = ContactSystem()
 
-    var network: BotNetworkHandler<*> = TIMBotNetworkHandler(this.coroutineContext, this)
+    lateinit var network: BotNetworkHandler<*>
 
     init {
         launch {
@@ -76,19 +88,40 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
      * [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
      * 然后重新启动并尝试登录
      */
-    @JvmOverloads
-    suspend fun reinitializeNetworkHandler(
+    @JvmOverloads // shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
+    fun tryReinitializeNetworkHandler(
+        configuration: BotConfiguration,
+        cause: Throwable? = null
+    ): Job = launch {
+        repeat(configuration.reconnectionRetryTimes) {
+            if (reinitializeNetworkHandlerAsync(configuration, cause).await().isSuccess()) {
+                logger.info("Reconnected successfully")
+                return@launch
+            } else {
+                delay(configuration.reconnectPeriod.millisecondsLong)
+            }
+        }
+    }
+
+    /**
+     * [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
+     * 然后重新启动并尝试登录
+     */
+    @JvmOverloads // shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
+    fun reinitializeNetworkHandlerAsync(
         configuration: BotConfiguration,
         cause: Throwable? = null
-    ): LoginResult {
+    ): Deferred<LoginResult> = async {
         logger.info("Initializing BotNetworkHandler")
         try {
-            network.close(cause)
+            if (::network.isInitialized) {
+                network.close(cause)
+            }
         } catch (e: Exception) {
-            logger.error(e)
+            logger.error("Cannot close network handler", e)
         }
-        network = TIMBotNetworkHandler(this.coroutineContext, this)
-        return network.login(configuration)
+        network = TIMBotNetworkHandler(coroutineContext + configuration, this@Bot)
+        network.login()
     }
 
     /**

+ 3 - 3
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotHelper.kt

@@ -70,13 +70,13 @@ suspend inline fun Bot.login(noinline configuration: BotConfiguration.() -> Unit
     contract {
         callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
     }
-    return this.network.login(BotConfiguration().apply(configuration))
+    return this.reinitializeNetworkHandlerAsync(BotConfiguration().apply(configuration)).await()
 }
 
 /**
  * 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回登录结果
  */
-suspend inline fun Bot.login(): LoginResult = this.network.login(BotConfiguration.Default)
+suspend inline fun Bot.login(): LoginResult = this.reinitializeNetworkHandlerAsync(BotConfiguration.Default).await()
 
 /**
  * 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回 [this]
@@ -91,7 +91,7 @@ suspend inline fun Bot.alsoLogin(noinline configuration: BotConfiguration.() ->
     contract {
         callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
     }
-    this.network.login(BotConfiguration().apply(configuration)).requireSuccess()
+    this.reinitializeNetworkHandlerAsync(BotConfiguration().apply(configuration)).await().requireSuccess()
     return this
 }
 

+ 4 - 6
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt

@@ -1,9 +1,8 @@
 package net.mamoe.mirai.network
 
 import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableJob
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancelChildren
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocketAdapter
 import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
@@ -12,7 +11,6 @@ import net.mamoe.mirai.network.protocol.tim.packet.Packet
 import net.mamoe.mirai.network.protocol.tim.packet.login.HeartbeatPacket
 import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
 import net.mamoe.mirai.network.protocol.tim.packet.login.RequestSKeyPacket
-import net.mamoe.mirai.utils.BotConfiguration
 import net.mamoe.mirai.utils.io.PlatformDatagramChannel
 
 /**
@@ -38,7 +36,7 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
     val socket: Socket
     val bot: Bot
 
-    val supervisor get() = SupervisorJob()
+    val supervisor: CompletableJob
 
     val session: BotSession
 
@@ -46,7 +44,7 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
      * 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回登录结果
      * 本函数将挂起直到登录成功.
      */
-    suspend fun login(configuration: BotConfiguration): LoginResult
+    suspend fun login(): LoginResult
 
     /**
      * 添加一个临时包处理器, 并发送相应的包
@@ -70,6 +68,6 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
      * 关闭网络接口, 停止所有有关协程和任务
      */
     suspend fun close(cause: Throwable? = null) {
-        supervisor.cancelChildren(CancellationException("handler closed", cause))
+        supervisor.cancel(CancellationException("handler closed", cause))
     }
 }

+ 24 - 44
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/TIMBotNetworkHandler.kt

@@ -20,8 +20,8 @@ import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler
 import net.mamoe.mirai.network.protocol.tim.packet.*
 import net.mamoe.mirai.network.protocol.tim.packet.login.*
 import net.mamoe.mirai.qqAccount
-import net.mamoe.mirai.utils.BotConfiguration
 import net.mamoe.mirai.utils.OnlineStatus
+import net.mamoe.mirai.utils.currentBotConfiguration
 import net.mamoe.mirai.utils.io.*
 import kotlin.coroutines.CoroutineContext
 import kotlin.properties.Delegates
@@ -42,6 +42,7 @@ internal expect val NetworkDispatcher: CoroutineDispatcher
 internal class TIMBotNetworkHandler internal constructor(coroutineContext: CoroutineContext, override inline val bot: Bot) :
     BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, CoroutineScope {
 
+    override val supervisor: CompletableJob = SupervisorJob(coroutineContext[Job])
 
     override val coroutineContext: CoroutineContext =
         coroutineContext + NetworkDispatcher + CoroutineExceptionHandler { context, e ->
@@ -49,7 +50,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                 ?: "an unnamed coroutine"} under TIMBotNetworkHandler", e)
         } + supervisor
 
-
     override lateinit var socket: BotSocketAdapter
         private set
 
@@ -60,7 +60,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
 
     private lateinit var userContext: CoroutineContext
 
-
     override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*, *>) {
         handlersLock.withLock {
             temporaryPacketHandlers.add(temporaryPacketHandler)
@@ -68,14 +67,14 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
         temporaryPacketHandler.send(this.session)
     }
 
-    override suspend fun login(configuration: BotConfiguration): LoginResult {
+    override suspend fun login(): LoginResult {
         userContext = coroutineContext
         return withContext(this.coroutineContext) {
             TIMProtocol.SERVER_IP.sortedBy { Random.nextInt() }.forEach { ip ->
                 bot.logger.info("Connecting server $ip")
                 try {
                     withTimeout(3000) {
-                        socket = BotSocketAdapter(ip, configuration)
+                        socket = BotSocketAdapter(ip)
                     }
                 } catch (e: Exception) {
                     return@withContext LoginResult.NETWORK_UNAVAILABLE
@@ -126,7 +125,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
 
     override suspend fun sendPacket(packet: OutgoingPacket) = socket.sendPacket(packet)
 
-    internal inner class BotSocketAdapter(override val serverIp: String, val configuration: BotConfiguration) :
+    internal inner class BotSocketAdapter(override val serverIp: String) :
         DataPacketSocketAdapter {
 
         override val channel: PlatformDatagramChannel = PlatformDatagramChannel(serverIp, 8000)
@@ -202,13 +201,13 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
         internal suspend fun resendTouch(): LoginResult /* = coroutineScope */ {
             loginHandler?.close()
 
-            loginHandler = LoginHandler(configuration)
+            loginHandler = LoginHandler()
 
 
             val expect = expectPacket<TouchPacket.TouchResponse>()
             launch { processReceive() }
             launch {
-                if (withTimeoutOrNull(configuration.touchTimeout.millisecondsLong) { expect.join() } == null) {
+                if (withTimeoutOrNull(currentBotConfiguration().touchTimeout.millisecondsLong) { expect.join() } == null) {
                     loginResult.complete(LoginResult.TIMEOUT)
                 }
             }
@@ -284,10 +283,9 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                     if (e.cause !is CancellationException) {
                         bot.logger.error("Caught SendPacketInternalException: ${e.cause?.message}")
                     }
-
-                    GlobalScope.launch(userContext) {
-                        bot.reinitializeNetworkHandler(configuration, e)
-                    }
+                    val configuration = currentBotConfiguration()
+                    delay(configuration.firstReconnectDelay.millisecondsLong)
+                    bot.tryReinitializeNetworkHandler(configuration, e)
                     return@withContext
                 } finally {
                     buffer.release(IoBuffer.Pool)
@@ -319,7 +317,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
     /**
      * 处理登录过程
      */
-    inner class LoginHandler(private val configuration: BotConfiguration) {
+    inner class LoginHandler {
         private lateinit var token00BA: ByteArray
         private lateinit var token0825: ByteArray//56
         private var loginTime: Int = 0
@@ -375,7 +373,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                             privateKey = privateKey,
                             token0825 = token0825,
                             token00BA = null,
-                            randomDeviceName = socket.configuration.randomDeviceName
+                            randomDeviceName = currentBotConfiguration().randomDeviceName
                         )
                     )
                 }
@@ -383,7 +381,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                 is TouchPacket.TouchResponse.Redirection -> {
                     withContext(userContext) {
                         socket.close()
-                        socket = BotSocketAdapter(packet.serverIP!!, socket.configuration)
+                        socket = BotSocketAdapter(packet.serverIP!!)
                         bot.logger.info("Redirecting to ${packet.serverIP}")
                         loginResult.complete(socket.resendTouch())
                     }
@@ -407,7 +405,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                             privateKey = privateKey,
                             token0825 = token0825,
                             token00BA = packet.token00BA,
-                            randomDeviceName = socket.configuration.randomDeviceName
+                            randomDeviceName = currentBotConfiguration().randomDeviceName
                         )
                     )
                 }
@@ -441,6 +439,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                     this.captchaCache!!.writeFully(packet.captchaSectionN)
                     this.token00BA = packet.token00BA
 
+                    val configuration = currentBotConfiguration()
                     if (packet.transmissionCompleted) {
                         if (configuration.failOnCaptcha) {
                             loginResult.complete(LoginResult.CAPTCHA)
@@ -455,23 +454,11 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                             socket.sendPacket(CaptchaPacket.Refresh(bot.qqAccount, token0825))
                         } else {
                             this.captchaSectionId = 0//意味着已经提交验证码
-                            socket.sendPacket(
-                                CaptchaPacket.Submit(
-                                    bot.qqAccount,
-                                    token0825,
-                                    code,
-                                    packet.captchaToken
-                                )
-                            )
+                            socket.sendPacket(CaptchaPacket.Submit(bot.qqAccount, token0825, code, packet.captchaToken))
                         }
                     } else {
                         socket.sendPacket(
-                            CaptchaPacket.RequestTransmission(
-                                bot.qqAccount,
-                                token0825,
-                                captchaSectionId++,
-                                packet.token00BA
-                            )
+                            CaptchaPacket.RequestTransmission(bot.qqAccount, token0825, captchaSectionId++, packet.token00BA)
                         )
                     }
                 }
@@ -479,13 +466,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                 is SubmitPasswordPacket.LoginResponse.Success -> {
                     this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey
                     socket.sendPacket(
-                        RequestSessionPacket(
-                            bot.qqAccount,
-                            socket.serverIp,
-                            packet.token38,
-                            packet.token88,
-                            packet.encryptionKey
-                        )
+                        RequestSessionPacket(bot.qqAccount, socket.serverIp, packet.token38, packet.token88, packet.encryptionKey)
                     )
                 }
 
@@ -502,7 +483,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                             privateKey = privateKey,
                             token0825 = token0825,
                             token00BA = packet.tokenUnknown ?: token00BA,
-                            randomDeviceName = socket.configuration.randomDeviceName,
+                            randomDeviceName = currentBotConfiguration().randomDeviceName,
                             tlv0006 = packet.tlv0006
                         )
                     )
@@ -512,6 +493,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                     sessionKey = packet.sessionKey
                     bot.logger.info("sessionKey = ${sessionKey.value.toUHexString()}")
 
+                    val configuration = currentBotConfiguration()
                     heartbeatJob = [email protected] {
                         while (socket.isOpen) {
                             delay(configuration.heartbeatPeriod.millisecondsLong)
@@ -519,15 +501,13 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
                                 class HeartbeatTimeoutException : CancellationException("heartbeat timeout")
 
                                 if (withTimeoutOrNull(configuration.heartbeatTimeout.millisecondsLong) {
-                                        // TODO: 2019/11/26 启动被挤掉线检测
+                                        // FIXME: 2019/11/26 启动被挤掉线检测
 
-                                        HeartbeatPacket(
-                                            bot.qqAccount,
-                                            sessionKey
-                                        ).sendAndExpect<HeartbeatPacketResponse>()
+                                        HeartbeatPacket(bot.qqAccount, sessionKey).sendAndExpect<HeartbeatPacketResponse>()
                                     } == null) {
                                     bot.logger.warning("Heartbeat timed out")
-                                    bot.reinitializeNetworkHandler(configuration, HeartbeatTimeoutException())
+                                    delay(configuration.firstReconnectDelay.millisecondsLong)
+                                    bot.tryReinitializeNetworkHandler(configuration, HeartbeatTimeoutException())
                                     return@launch
                                 }
                             }

+ 7 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/LoginResult.kt

@@ -123,6 +123,13 @@ fun LoginResult.requireSuccess() = requireSuccess { "Login failed: $this" }
  */
 fun LoginResult.requireSuccessOrNull(): Unit? = if (this == SUCCESS) Unit else null
 
+/**
+ * 返回 [this] 是否为 [LoginResult.SUCCESS].
+ */
+@Suppress("NOTHING_TO_INLINE")
+@UseExperimental(ExperimentalContracts::class)
+inline fun LoginResult.isSuccess(): Boolean = this == SUCCESS
+
 /**
  * 检查 [this] 为 [LoginResult.SUCCESS].
  * 失败则返回 `null`

+ 21 - 3
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt

@@ -5,6 +5,8 @@ import com.soywiz.klock.seconds
 import kotlinx.io.core.IoBuffer
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.network.protocol.tim.packet.login.TouchPacket.TouchResponse
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
 import kotlin.jvm.JvmField
 
 /**
@@ -22,7 +24,7 @@ expect var DefaultCaptchaSolver: CaptchaSolver
 /**
  * 网络和连接配置
  */
-class BotConfiguration {
+class BotConfiguration : CoroutineContext.Element {
     /**
      * 等待 [TouchResponse] 的时间
      */
@@ -42,6 +44,18 @@ class BotConfiguration {
      * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
      */
     var heartbeatTimeout: TimeSpan = 2.seconds
+    /**
+     * 心跳失败后的第一次重连前的等待时间.
+     */
+    var firstReconnectDelay: TimeSpan = 5.seconds
+    /**
+     * 重连失败后, 继续尝试的每次等待时间
+     */
+    var reconnectPeriod: TimeSpan = 60.seconds
+    /**
+     * 最多尝试多少次重连
+     */
+    var reconnectionRetryTimes: Int = 3
     /**
      * 有验证码要求就失败
      */
@@ -51,11 +65,15 @@ class BotConfiguration {
      */
     var captchaSolver: CaptchaSolver = DefaultCaptchaSolver
 
-    companion object {
+    companion object Key : CoroutineContext.Key<BotConfiguration> {
         /**
          * 默认的配置实例
          */
         @JvmField
         val Default = BotConfiguration()
     }
-}
+
+    override val key: CoroutineContext.Key<*> get() = Key
+}
+
+suspend inline fun currentBotConfiguration(): BotConfiguration = coroutineContext[BotConfiguration] ?: error("No BotConfiguration found")

+ 4 - 3
mirai-debug/src/main/kotlin/PacketDebuger.kt

@@ -17,7 +17,6 @@ import net.mamoe.mirai.network.protocol.tim.packet.login.CaptchaKey
 import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
 import net.mamoe.mirai.network.protocol.tim.packet.login.ShareKey
 import net.mamoe.mirai.network.protocol.tim.packet.login.TouchKey
-import net.mamoe.mirai.utils.BotConfiguration
 import net.mamoe.mirai.utils.DecryptionFailedException
 import net.mamoe.mirai.utils.decryptBy
 import net.mamoe.mirai.utils.io.*
@@ -301,6 +300,7 @@ when (idHex.substring(0, 5)) {
 
 
 internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>, CoroutineScope {
+    override val supervisor: CompletableJob = SupervisorJob()
     override val socket: DataPacketSocketAdapter = object : DataPacketSocketAdapter {
         override val serverIp: String
             get() = ""
@@ -320,10 +320,10 @@ internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>
             get() = bot
 
     }
-    override val bot: Bot = Bot(qq, "")
+    override val bot: Bot = Bot(qq, "", coroutineContext)
     override val session = BotSession(bot, sessionKey, socket, this)
 
-    override suspend fun login(configuration: BotConfiguration): LoginResult = LoginResult.SUCCESS
+    override suspend fun login(): LoginResult = LoginResult.SUCCESS
 
     override suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*, *>) {
     }
@@ -336,4 +336,5 @@ internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>
 
     override val coroutineContext: CoroutineContext
         get() = GlobalScope.coroutineContext
+
 }