BotImpl.kt 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. * Copyright 2020 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/master/LICENSE
  8. */
  9. @file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR", "OverridingDeprecatedMember")
  10. package net.mamoe.mirai
  11. import kotlinx.coroutines.*
  12. import net.mamoe.mirai.event.Listener
  13. import net.mamoe.mirai.event.broadcast
  14. import net.mamoe.mirai.event.events.BotOfflineEvent
  15. import net.mamoe.mirai.event.events.BotReloginEvent
  16. import net.mamoe.mirai.event.subscribeAlways
  17. import net.mamoe.mirai.network.BotNetworkHandler
  18. import net.mamoe.mirai.network.ForceOfflineException
  19. import net.mamoe.mirai.network.LoginFailedException
  20. import net.mamoe.mirai.network.closeAndJoin
  21. import net.mamoe.mirai.utils.*
  22. import net.mamoe.mirai.utils.internal.retryCatching
  23. import kotlin.coroutines.CoroutineContext
  24. /*
  25. * 泛型 N 不需要向外(接口)暴露.
  26. */
  27. @OptIn(MiraiExperimentalAPI::class)
  28. @MiraiInternalAPI
  29. abstract class BotImpl<N : BotNetworkHandler> constructor(
  30. context: Context,
  31. val configuration: BotConfiguration
  32. ) : Bot(), CoroutineScope {
  33. final override val coroutineContext: CoroutineContext =
  34. configuration.parentCoroutineContext + SupervisorJob(configuration.parentCoroutineContext[Job]) +
  35. (configuration.parentCoroutineContext[CoroutineExceptionHandler]
  36. ?: CoroutineExceptionHandler { _, e ->
  37. logger.error(
  38. "An exception was thrown under a coroutine of Bot",
  39. e
  40. )
  41. })
  42. override val context: Context by context.unsafeWeakRef()
  43. @Deprecated("use id instead", replaceWith = ReplaceWith("id"))
  44. override val uin: Long
  45. get() = this.id
  46. final override val logger: MiraiLogger by lazy { configuration.botLoggerSupplier(this) }
  47. init {
  48. instances.addLast(this.weakRef())
  49. }
  50. companion object {
  51. @PublishedApi
  52. internal val instances: LockFreeLinkedList<WeakRef<Bot>> = LockFreeLinkedList()
  53. inline fun forEachInstance(block: (Bot) -> Unit) = instances.forEach {
  54. it.get()?.let(block)
  55. }
  56. fun getInstance(qq: Long): Bot {
  57. instances.forEach {
  58. it.get()?.let { bot ->
  59. if (bot.id == qq) {
  60. return bot
  61. }
  62. }
  63. }
  64. throw NoSuchElementException()
  65. }
  66. }
  67. // region network
  68. final override val network: N get() = _network
  69. @Suppress("PropertyName")
  70. internal lateinit var _network: N
  71. /**
  72. * Close server connection, resend login packet, BUT DOESN'T [BotNetworkHandler.init]
  73. */
  74. @ThisApiMustBeUsedInWithConnectionLockBlock
  75. @Throws(LoginFailedException::class) // only
  76. protected abstract suspend fun relogin(cause: Throwable?)
  77. @Suppress("unused")
  78. private val offlineListener: Listener<BotOfflineEvent> =
  79. [email protected](concurrency = Listener.ConcurrencyKind.LOCKED) { event ->
  80. if (network.areYouOk() && event !is BotOfflineEvent.Force) {
  81. // avoid concurrent re-login tasks
  82. return@subscribeAlways
  83. }
  84. when (event) {
  85. is BotOfflineEvent.Dropped,
  86. is BotOfflineEvent.RequireReconnect
  87. -> {
  88. if (!_network.isActive) {
  89. // normally closed
  90. return@subscribeAlways
  91. }
  92. bot.logger.info { "Connection dropped by server or lost, retrying login" }
  93. tailrec suspend fun reconnect() {
  94. retryCatching<Unit>(configuration.reconnectionRetryTimes,
  95. except = LoginFailedException::class) { tryCount, _ ->
  96. if (tryCount != 0) {
  97. delay(configuration.reconnectPeriodMillis)
  98. }
  99. network.withConnectionLock {
  100. /**
  101. * [BotImpl.relogin] only, no [BotNetworkHandler.init]
  102. */
  103. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  104. relogin((event as? BotOfflineEvent.Dropped)?.cause)
  105. }
  106. logger.info { "Reconnected successfully" }
  107. BotReloginEvent(bot, (event as? BotOfflineEvent.Dropped)?.cause).broadcast()
  108. return
  109. }.getOrElse {
  110. if (it is LoginFailedException && !it.killBot) {
  111. logger.info { "Cannot reconnect" }
  112. logger.warning(it)
  113. logger.info { "Retrying in 3s..." }
  114. delay(3000)
  115. return@getOrElse
  116. }
  117. logger.info { "Cannot reconnect" }
  118. throw it
  119. }
  120. reconnect()
  121. }
  122. reconnect()
  123. }
  124. is BotOfflineEvent.Active -> {
  125. val msg = if (event.cause == null) {
  126. ""
  127. } else {
  128. " with exception: " + event.cause.message
  129. }
  130. bot.logger.info { "Bot is closed manually$msg" }
  131. closeAndJoin(CancellationException(event.toString()))
  132. }
  133. is BotOfflineEvent.Force -> {
  134. bot.logger.info { "Connection occupied by another android device: ${event.message}" }
  135. closeAndJoin(ForceOfflineException(event.toString()))
  136. }
  137. }
  138. }
  139. /**
  140. * **Exposed public API**
  141. * [BotImpl.relogin] && [BotNetworkHandler.init]
  142. */
  143. final override suspend fun login() {
  144. @ThisApiMustBeUsedInWithConnectionLockBlock
  145. suspend fun reinitializeNetworkHandler(cause: Throwable?) {
  146. suspend fun doRelogin() {
  147. while (true) {
  148. _network = createNetworkHandler(this.coroutineContext)
  149. try {
  150. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  151. relogin(null)
  152. return
  153. } catch (e: LoginFailedException) {
  154. if (e.killBot) {
  155. throw e
  156. } else {
  157. logger.warning("Login failed. Retrying in 3s...")
  158. _network.closeAndJoin(e)
  159. delay(3000)
  160. continue
  161. }
  162. } catch (e: Exception) {
  163. network.logger.error(e)
  164. _network.closeAndJoin(e)
  165. }
  166. logger.warning("Login failed. Retrying in 3s...")
  167. delay(3000)
  168. }
  169. }
  170. suspend fun doInit() {
  171. retryCatching(2) { count, lastException ->
  172. if (count != 0) {
  173. if (!isActive) {
  174. logger.error("Cannot init due to fatal error")
  175. if (lastException == null) {
  176. logger.error("<no exception>")
  177. } else {
  178. logger.error(lastException)
  179. }
  180. }
  181. logger.warning("Init failed. Retrying in 3s...")
  182. delay(3000)
  183. }
  184. _network.init()
  185. }.getOrElse {
  186. network.logger.error(it)
  187. logger.error("Cannot init. some features may be affected")
  188. }
  189. }
  190. // logger.info("Initializing BotNetworkHandler")
  191. if (::_network.isInitialized) {
  192. BotReloginEvent(this, cause).broadcast()
  193. doRelogin()
  194. return
  195. }
  196. doRelogin()
  197. doInit()
  198. }
  199. logger.info("Logging in...")
  200. if (::_network.isInitialized) {
  201. network.withConnectionLock {
  202. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  203. reinitializeNetworkHandler(null)
  204. }
  205. } else {
  206. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  207. reinitializeNetworkHandler(null)
  208. }
  209. logger.info("Login successful")
  210. }
  211. protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
  212. // endregion
  213. init {
  214. coroutineContext[Job]!!.invokeOnCompletion { throwable ->
  215. network.close(throwable)
  216. offlineListener.cancel(CancellationException("bot cancelled", throwable))
  217. groups.delegate.clear() // job is cancelled, so child jobs are to be cancelled
  218. friends.delegate.clear()
  219. instances.removeIf { it.get()?.id == this.id }
  220. }
  221. }
  222. @OptIn(MiraiInternalAPI::class)
  223. override fun close(cause: Throwable?) {
  224. if (!this.isActive) {
  225. // already cancelled
  226. return
  227. }
  228. this.launch {
  229. BotOfflineEvent.Active(this@BotImpl, cause).broadcast()
  230. }
  231. if (cause == null) {
  232. this.cancel()
  233. } else {
  234. this.cancel(CancellationException("bot cancelled", cause))
  235. }
  236. }
  237. }
  238. @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
  239. internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock