AbstractBot.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /*
  2. * Copyright 2019-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(
  10. "EXPERIMENTAL_API_USAGE",
  11. "DEPRECATION_ERROR",
  12. "OverridingDeprecatedMember",
  13. "INVISIBLE_REFERENCE",
  14. "INVISIBLE_MEMBER"
  15. )
  16. package net.mamoe.mirai.internal
  17. import kotlinx.coroutines.*
  18. import net.mamoe.mirai.Bot
  19. import net.mamoe.mirai.event.Listener
  20. import net.mamoe.mirai.event.broadcast
  21. import net.mamoe.mirai.event.events.BotOfflineEvent
  22. import net.mamoe.mirai.event.events.BotReloginEvent
  23. import net.mamoe.mirai.event.subscribeAlways
  24. import net.mamoe.mirai.internal.network.BotNetworkHandler
  25. import net.mamoe.mirai.internal.network.DefaultServerList
  26. import net.mamoe.mirai.internal.network.closeAndJoin
  27. import net.mamoe.mirai.network.ForceOfflineException
  28. import net.mamoe.mirai.network.LoginFailedException
  29. import net.mamoe.mirai.supervisorJob
  30. import net.mamoe.mirai.utils.*
  31. import net.mamoe.mirai.utils.internal.retryCatching
  32. import kotlin.coroutines.CoroutineContext
  33. import kotlin.time.ExperimentalTime
  34. import kotlin.time.measureTime
  35. internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
  36. final override val configuration: BotConfiguration,
  37. final override val id: Long,
  38. ) : Bot, CoroutineScope {
  39. // FASTEST INIT
  40. final override val logger: MiraiLogger by lazy { configuration.botLoggerSupplier(this) }
  41. final override val coroutineContext: CoroutineContext = // for id
  42. configuration.parentCoroutineContext
  43. .plus(SupervisorJob(configuration.parentCoroutineContext[Job]))
  44. .plus(configuration.parentCoroutineContext[CoroutineExceptionHandler]
  45. ?: CoroutineExceptionHandler { _, e ->
  46. logger.error("An exception was thrown under a coroutine of Bot", e)
  47. }
  48. )
  49. .plus(CoroutineName("Mirai Bot"))
  50. init {
  51. @Suppress("LeakingThis")
  52. Bot._instances[this.id] = this
  53. supervisorJob.invokeOnCompletion {
  54. Bot._instances.remove(id)
  55. }
  56. }
  57. // region network
  58. val network: N get() = _network
  59. @Suppress("PropertyName")
  60. internal lateinit var _network: N
  61. override val isOnline: Boolean get() = _network.areYouOk()
  62. /**
  63. * Close server connection, resend login packet, BUT DOESN'T [BotNetworkHandler.init]
  64. */
  65. @ThisApiMustBeUsedInWithConnectionLockBlock
  66. @Throws(LoginFailedException::class) // only
  67. protected abstract suspend fun relogin(cause: Throwable?)
  68. @OptIn(ExperimentalTime::class)
  69. @Suppress("unused")
  70. private val offlineListener: Listener<BotOfflineEvent> =
  71. [email protected](concurrency = Listener.ConcurrencyKind.LOCKED) { event ->
  72. if (event.bot != this@AbstractBot) {
  73. return@subscribeAlways
  74. }
  75. if (!event.bot.isActive) {
  76. // bot closed
  77. return@subscribeAlways
  78. }
  79. if (!::_network.isInitialized) {
  80. // bot 还未登录就被 close
  81. return@subscribeAlways
  82. }
  83. /*
  84. if (network.areYouOk() && event !is BotOfflineEvent.Force && event !is BotOfflineEvent.MsfOffline) {
  85. // network 运行正常
  86. return@subscribeAlways
  87. }*/
  88. when (event) {
  89. is BotOfflineEvent.MsfOffline,
  90. is BotOfflineEvent.Dropped,
  91. is BotOfflineEvent.RequireReconnect,
  92. is BotOfflineEvent.PacketFactory10008
  93. -> {
  94. if (!_network.isActive) {
  95. // normally closed
  96. return@subscribeAlways
  97. }
  98. bot.logger.info { "Connection lost, retrying login" }
  99. bot.asQQAndroidBot().client.run {
  100. if (serverList.isEmpty()) {
  101. serverList.addAll(DefaultServerList)
  102. } else serverList.removeAt(0)
  103. }
  104. var failed = false
  105. val time = measureTime {
  106. tailrec suspend fun reconnect() {
  107. retryCatching<Unit>(
  108. configuration.reconnectionRetryTimes,
  109. except = LoginFailedException::class
  110. ) { tryCount, _ ->
  111. if (tryCount != 0) {
  112. delay(configuration.reconnectPeriodMillis)
  113. }
  114. network.withConnectionLock {
  115. /**
  116. * [AbstractBot.relogin] only, no [BotNetworkHandler.init]
  117. */
  118. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  119. relogin((event as? BotOfflineEvent.Dropped)?.cause)
  120. }
  121. launch {
  122. BotReloginEvent(
  123. bot,
  124. (event as? BotOfflineEvent.CauseAware)?.cause
  125. ).broadcast()
  126. }
  127. return
  128. }.getOrElse {
  129. if (it is LoginFailedException && !it.killBot) {
  130. logger.info { "Cannot reconnect." }
  131. logger.warning(it)
  132. logger.info { "Retrying in 3s..." }
  133. delay(3000)
  134. return@getOrElse
  135. }
  136. logger.info { "Cannot reconnect due to fatal error." }
  137. bot.cancel(CancellationException("Cannot reconnect due to fatal error.", it))
  138. failed = true
  139. return
  140. }
  141. reconnect()
  142. }
  143. reconnect()
  144. }
  145. if (!failed) {
  146. logger.info { "Reconnected successfully in ${time.toHumanReadableString()}" }
  147. }
  148. }
  149. is BotOfflineEvent.Active -> {
  150. val cause = event.cause
  151. val msg = if (cause == null) {
  152. ""
  153. } else {
  154. " with exception: " + cause.message
  155. }
  156. bot.logger.info { "Bot is closed manually: $msg" }
  157. bot.cancel(CancellationException(event.toString()))
  158. }
  159. is BotOfflineEvent.Force -> {
  160. bot.logger.info { "Connection occupied by another android device: ${event.message}" }
  161. bot.cancel(ForceOfflineException(event.toString()))
  162. }
  163. }
  164. }
  165. /**
  166. * **Exposed public API**
  167. * [AbstractBot.relogin] && [BotNetworkHandler.init]
  168. */
  169. final override suspend fun login() {
  170. @ThisApiMustBeUsedInWithConnectionLockBlock
  171. suspend fun reinitializeNetworkHandler(cause: Throwable?) {
  172. suspend fun doRelogin() {
  173. while (true) {
  174. _network = createNetworkHandler(this.coroutineContext)
  175. try {
  176. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  177. relogin(null)
  178. return
  179. } catch (e: LoginFailedException) {
  180. if (e.killBot) {
  181. throw e
  182. } else {
  183. logger.warning { "Login failed. Retrying in 3s..." }
  184. _network.closeAndJoin(e)
  185. delay(3000)
  186. continue
  187. }
  188. } catch (e: Exception) {
  189. network.logger.error(e)
  190. _network.closeAndJoin(e)
  191. }
  192. logger.warning { "Login failed. Retrying in 3s..." }
  193. delay(3000)
  194. }
  195. }
  196. suspend fun doInit() {
  197. retryCatching(5) { count, lastException ->
  198. if (count != 0) {
  199. if (!isActive) {
  200. logger.error("Cannot init due to fatal error")
  201. throw lastException ?: error("<No lastException>")
  202. }
  203. logger.warning { "Init failed. Retrying in 3s..." }
  204. delay(3000)
  205. }
  206. _network.init()
  207. }.getOrElse {
  208. logger.error { "Cannot init. some features may be affected" }
  209. throw it // abort
  210. }
  211. }
  212. // logger.info("Initializing BotNetworkHandler")
  213. if (::_network.isInitialized) {
  214. _network.cancel(CancellationException("manual re-login", cause = cause))
  215. BotReloginEvent(this, cause).broadcast()
  216. doRelogin()
  217. return
  218. }
  219. doRelogin()
  220. doInit()
  221. }
  222. logger.info { "Logging in..." }
  223. if (::_network.isInitialized) {
  224. network.withConnectionLock {
  225. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  226. reinitializeNetworkHandler(null)
  227. }
  228. } else {
  229. @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
  230. reinitializeNetworkHandler(null)
  231. }
  232. logger.info { "Login successful" }
  233. }
  234. protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
  235. // endregion
  236. init {
  237. coroutineContext[Job]!!.invokeOnCompletion { throwable ->
  238. logger.info { "Bot cancelled" + throwable?.message?.let { ": $it" }.orEmpty() }
  239. kotlin.runCatching {
  240. network.close(throwable)
  241. }
  242. offlineListener.cancel(CancellationException("Bot cancelled", throwable))
  243. // help GC release instances
  244. groups.forEach {
  245. it.members.delegate.clear()
  246. }
  247. groups.delegate.clear() // job is cancelled, so child jobs are to be cancelled
  248. friends.delegate.clear()
  249. }
  250. }
  251. override fun close(cause: Throwable?) {
  252. if (!this.isActive) {
  253. // already cancelled
  254. return
  255. }
  256. GlobalScope.launch {
  257. runCatching { BotOfflineEvent.Active(this@AbstractBot, cause).broadcast() }.exceptionOrNull()
  258. ?.let { logger.error(it) }
  259. }
  260. if (supervisorJob.isActive) {
  261. if (cause == null) {
  262. supervisorJob.cancel()
  263. } else {
  264. supervisorJob.cancel(CancellationException("Bot closed", cause))
  265. }
  266. }
  267. }
  268. final override fun toString(): String = "Bot($id)"
  269. }
  270. @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
  271. internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock