| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- /*
- * Copyright 2020 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
- */
- @file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR", "OverridingDeprecatedMember")
- package net.mamoe.mirai
- import kotlinx.coroutines.*
- import net.mamoe.mirai.event.Listener
- import net.mamoe.mirai.event.broadcast
- import net.mamoe.mirai.event.events.BotOfflineEvent
- import net.mamoe.mirai.event.events.BotReloginEvent
- import net.mamoe.mirai.event.subscribeAlways
- import net.mamoe.mirai.network.BotNetworkHandler
- import net.mamoe.mirai.network.ForceOfflineException
- import net.mamoe.mirai.network.LoginFailedException
- import net.mamoe.mirai.network.closeAndJoin
- import net.mamoe.mirai.utils.*
- import net.mamoe.mirai.utils.internal.retryCatching
- import kotlin.coroutines.CoroutineContext
- /*
- * 泛型 N 不需要向外(接口)暴露.
- */
- @OptIn(MiraiExperimentalAPI::class)
- @MiraiInternalAPI
- abstract class BotImpl<N : BotNetworkHandler> constructor(
- context: Context,
- val configuration: BotConfiguration
- ) : Bot(), CoroutineScope {
- final override val coroutineContext: CoroutineContext =
- configuration.parentCoroutineContext + SupervisorJob(configuration.parentCoroutineContext[Job]) +
- (configuration.parentCoroutineContext[CoroutineExceptionHandler]
- ?: CoroutineExceptionHandler { _, e ->
- logger.error(
- "An exception was thrown under a coroutine of Bot",
- e
- )
- })
- override val context: Context by context.unsafeWeakRef()
- @Deprecated("use id instead", replaceWith = ReplaceWith("id"))
- override val uin: Long
- get() = this.id
- final override val logger: MiraiLogger by lazy { configuration.botLoggerSupplier(this) }
- init {
- instances.addLast(this.weakRef())
- }
- companion object {
- @PublishedApi
- internal val instances: LockFreeLinkedList<WeakRef<Bot>> = LockFreeLinkedList()
- inline fun forEachInstance(block: (Bot) -> Unit) = instances.forEach {
- it.get()?.let(block)
- }
- fun getInstance(qq: Long): Bot {
- instances.forEach {
- it.get()?.let { bot ->
- if (bot.id == qq) {
- return bot
- }
- }
- }
- throw NoSuchElementException()
- }
- }
- // region network
- final override val network: N get() = _network
- @Suppress("PropertyName")
- internal lateinit var _network: N
- /**
- * Close server connection, resend login packet, BUT DOESN'T [BotNetworkHandler.init]
- */
- @ThisApiMustBeUsedInWithConnectionLockBlock
- @Throws(LoginFailedException::class) // only
- protected abstract suspend fun relogin(cause: Throwable?)
- @Suppress("unused")
- private val offlineListener: Listener<BotOfflineEvent> =
- [email protected](concurrency = Listener.ConcurrencyKind.LOCKED) { event ->
- if (network.areYouOk() && event !is BotOfflineEvent.Force) {
- // avoid concurrent re-login tasks
- return@subscribeAlways
- }
- when (event) {
- is BotOfflineEvent.Dropped,
- is BotOfflineEvent.RequireReconnect
- -> {
- if (!_network.isActive) {
- // normally closed
- return@subscribeAlways
- }
- bot.logger.info { "Connection dropped by server or lost, retrying login" }
- tailrec suspend fun reconnect() {
- retryCatching<Unit>(configuration.reconnectionRetryTimes,
- except = LoginFailedException::class) { tryCount, _ ->
- if (tryCount != 0) {
- delay(configuration.reconnectPeriodMillis)
- }
- network.withConnectionLock {
- /**
- * [BotImpl.relogin] only, no [BotNetworkHandler.init]
- */
- @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
- relogin((event as? BotOfflineEvent.Dropped)?.cause)
- }
- logger.info { "Reconnected successfully" }
- BotReloginEvent(bot, (event as? BotOfflineEvent.Dropped)?.cause).broadcast()
- return
- }.getOrElse {
- if (it is LoginFailedException && !it.killBot) {
- logger.info { "Cannot reconnect" }
- logger.warning(it)
- logger.info { "Retrying in 3s..." }
- delay(3000)
- return@getOrElse
- }
- logger.info { "Cannot reconnect" }
- throw it
- }
- reconnect()
- }
- reconnect()
- }
- is BotOfflineEvent.Active -> {
- val msg = if (event.cause == null) {
- ""
- } else {
- " with exception: " + event.cause.message
- }
- bot.logger.info { "Bot is closed manually$msg" }
- closeAndJoin(CancellationException(event.toString()))
- }
- is BotOfflineEvent.Force -> {
- bot.logger.info { "Connection occupied by another android device: ${event.message}" }
- closeAndJoin(ForceOfflineException(event.toString()))
- }
- }
- }
- /**
- * **Exposed public API**
- * [BotImpl.relogin] && [BotNetworkHandler.init]
- */
- final override suspend fun login() {
- @ThisApiMustBeUsedInWithConnectionLockBlock
- suspend fun reinitializeNetworkHandler(cause: Throwable?) {
- suspend fun doRelogin() {
- while (true) {
- _network = createNetworkHandler(this.coroutineContext)
- try {
- @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
- relogin(null)
- return
- } catch (e: LoginFailedException) {
- if (e.killBot) {
- throw e
- } else {
- logger.warning("Login failed. Retrying in 3s...")
- _network.closeAndJoin(e)
- delay(3000)
- continue
- }
- } catch (e: Exception) {
- network.logger.error(e)
- _network.closeAndJoin(e)
- }
- logger.warning("Login failed. Retrying in 3s...")
- delay(3000)
- }
- }
- suspend fun doInit() {
- retryCatching(2) { count, lastException ->
- if (count != 0) {
- if (!isActive) {
- logger.error("Cannot init due to fatal error")
- if (lastException == null) {
- logger.error("<no exception>")
- } else {
- logger.error(lastException)
- }
- }
- logger.warning("Init failed. Retrying in 3s...")
- delay(3000)
- }
- _network.init()
- }.getOrElse {
- network.logger.error(it)
- logger.error("Cannot init. some features may be affected")
- }
- }
- // logger.info("Initializing BotNetworkHandler")
- if (::_network.isInitialized) {
- BotReloginEvent(this, cause).broadcast()
- doRelogin()
- return
- }
- doRelogin()
- doInit()
- }
- logger.info("Logging in...")
- if (::_network.isInitialized) {
- network.withConnectionLock {
- @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
- reinitializeNetworkHandler(null)
- }
- } else {
- @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
- reinitializeNetworkHandler(null)
- }
- logger.info("Login successful")
- }
- protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
- // endregion
- init {
- coroutineContext[Job]!!.invokeOnCompletion { throwable ->
- network.close(throwable)
- offlineListener.cancel(CancellationException("bot cancelled", throwable))
- groups.delegate.clear() // job is cancelled, so child jobs are to be cancelled
- friends.delegate.clear()
- instances.removeIf { it.get()?.id == this.id }
- }
- }
- @OptIn(MiraiInternalAPI::class)
- override fun close(cause: Throwable?) {
- if (!this.isActive) {
- // already cancelled
- return
- }
- this.launch {
- BotOfflineEvent.Active(this@BotImpl, cause).broadcast()
- }
- if (cause == null) {
- this.cancel()
- } else {
- this.cancel(CancellationException("bot cancelled", cause))
- }
- }
- }
- @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
- internal annotation class ThisApiMustBeUsedInWithConnectionLockBlock
|