mirai/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt
2020-02-28 19:16:22 +08:00

196 lines
6.5 KiB
Kotlin

/*
* 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")
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 kotlin.coroutines.CoroutineContext
/*
* 泛型 N 不需要向外(接口)暴露.
*/
@UseExperimental(MiraiExperimentalAPI::class)
@MiraiInternalAPI
abstract class BotImpl<N : BotNetworkHandler> constructor(
context: Context,
account: BotAccount,
val configuration: BotConfiguration
) : Bot(), CoroutineScope {
private val botJob = SupervisorJob(configuration.parentCoroutineContext[Job])
override val coroutineContext: CoroutineContext =
configuration.parentCoroutineContext + botJob + (configuration.parentCoroutineContext[CoroutineExceptionHandler]
?: CoroutineExceptionHandler { _, e -> logger.error("An exception was thrown under a coroutine of Bot", e) })
override val context: Context by context.unsafeWeakRef()
@Suppress("CanBePrimaryConstructorProperty") // for logger
final override val account: BotAccount = account
@UseExperimental(RawAccountIdUse::class)
override val uin: Long
get() = account.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.uin == qq) {
return bot
}
}
}
throw NoSuchElementException()
}
}
// region network
final override val network: N get() = _network
@Suppress("PropertyName")
internal lateinit var _network: N
@Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
when (event) {
is BotOfflineEvent.Dropped -> {
if (!_network.isActive) {
return@subscribeAlways
}
bot.logger.info("Connection dropped by server or lost, retrying login")
tryNTimesOrException(configuration.reconnectionRetryTimes) { tryCount ->
if (tryCount != 0) {
delay(configuration.reconnectPeriodMillis)
}
network.relogin(event.cause)
logger.info("Reconnected successfully")
BotReloginEvent(bot, event.cause).broadcast()
return@subscribeAlways
}?.let {
logger.info("Cannot reconnect")
throw it
}
}
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()))
}
}
}
final override suspend fun login() {
logger.info("Logging in...")
reinitializeNetworkHandler(null)
logger.info("Login successful")
}
private suspend fun reinitializeNetworkHandler(
cause: Throwable?
) {
suspend fun doRelogin() {
while (true) {
_network = createNetworkHandler(this.coroutineContext)
try {
_network.relogin()
return
} catch (e: LoginFailedException) {
throw e
} catch (e: Exception) {
network.logger.error(e)
_network.closeAndJoin(e)
}
logger.warning("Login failed. Retrying in 3s...")
delay(3000)
}
}
suspend fun doInit() {
tryNTimesOrException(2) {
if (it != 0) {
delay(3000)
logger.warning("Init failed. Retrying in 3s...")
}
_network.init()
}?.let {
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()
}
protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
// endregion
@UseExperimental(MiraiInternalAPI::class)
override fun close(cause: Throwable?) {
if (!this.botJob.isActive) {
// already cancelled
return
}
kotlin.runCatching {
if (cause == null) {
this.botJob.cancel()
network.close()
offlineListener.cancel()
} else {
this.botJob.cancel(CancellationException("bot cancelled", cause))
network.close(cause)
offlineListener.cancel(CancellationException("bot cancelled", cause))
}
}
groups.delegate.clear()
qqs.delegate.clear()
instances.removeIf { it.get()?.uin == this.uin }
}
}