Change Bot to interface

This commit is contained in:
Him188 2020-12-17 09:08:26 +08:00
parent b0d43eb708
commit 9c71a9c953
4 changed files with 181 additions and 114 deletions

View File

@ -20,8 +20,12 @@ import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.action.BotNudge import net.mamoe.mirai.message.action.BotNudge
import net.mamoe.mirai.message.action.MemberNudge import net.mamoe.mirai.message.action.MemberNudge
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.BotConfiguration
import kotlin.coroutines.CoroutineContext import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.PlannedRemoval
import java.util.*
import kotlin.NoSuchElementException
/** /**
* 登录, 返回 [this] * 登录, 返回 [this]
@ -40,87 +44,31 @@ public suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() }
* *
* @see BotFactory 构造 [Bot] 的工厂, [Bot] 唯一的构造方式. * @see BotFactory 构造 [Bot] 的工厂, [Bot] 唯一的构造方式.
*/ */
public abstract class Bot internal constructor( public interface Bot : CoroutineScope, ContactOrBot, UserOrBot {
/**
* Bot 配置
*/
public val configuration: BotConfiguration public val configuration: BotConfiguration
) : CoroutineScope, ContactOrBot, UserOrBot {
public final override val coroutineContext: CoroutineContext = // for id
configuration.parentCoroutineContext
.plus(SupervisorJob(configuration.parentCoroutineContext[Job]))
.plus(configuration.parentCoroutineContext[CoroutineExceptionHandler]
?: CoroutineExceptionHandler { _, e ->
logger.error("An exception was thrown under a coroutine of Bot", e)
}
)
.plus(CoroutineName("Mirai Bot"))
public companion object {
@JvmField
@Suppress("ObjectPropertyName")
internal val _instances: LockFreeLinkedList<WeakRef<Bot>> = LockFreeLinkedList()
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
public val botInstances: List<Bot>
get() = _instances.asSequence().mapNotNull { it.get() }.toList()
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
public val botInstancesSequence: Sequence<Bot>
get() = _instances.asSequence().mapNotNull { it.get() }
/**
* 遍历每一个 [Bot] 实例
*/
@JvmSynthetic
public fun forEachInstance(block: (Bot) -> Unit): Unit = _instances.forEach { it.get()?.let(block) }
/**
* 获取一个 [Bot] 实例, 无对应实例时抛出 [NoSuchElementException]
*/
@JvmStatic
@Throws(NoSuchElementException::class)
public fun getInstance(qq: Long): Bot =
getInstanceOrNull(qq) ?: throw NoSuchElementException(qq.toString())
/**
* 获取一个 [Bot] 实例, 无对应实例时返回 `null`
*/
@JvmStatic
public fun getInstanceOrNull(qq: Long): Bot? =
_instances.asSequence().mapNotNull { it.get() }.firstOrNull { it.id == qq }
}
init {
_instances.addLast(this.weakRef())
supervisorJob.invokeOnCompletion {
_instances.removeIf { it.get()?.id == this.id }
}
}
/** /**
* QQ 号码. 实际类型为 uint * QQ 号码. 实际类型为 uint
*/ */
public abstract override val id: Long public override val id: Long
/** /**
* 昵称 * 昵称
*/ */
public abstract val nick: String public val nick: String
/** /**
* 日志记录器 * 日志记录器
*/ */
public abstract val logger: MiraiLogger public val logger: MiraiLogger
/** /**
* 判断 Bot 是否在线 (可正常收发消息) * Bot 在线 (可正常收发消息) 时返回 `true`.
*/ */
public abstract val isOnline: Boolean public val isOnline: Boolean
// region contacts // region contacts
@ -128,18 +76,18 @@ public abstract class Bot internal constructor(
* [User.id] [Bot.id] 相同的 [Friend] 实例 * [User.id] [Bot.id] 相同的 [Friend] 实例
*/ */
@MiraiExperimentalApi @MiraiExperimentalApi
public abstract val asFriend: Friend public val asFriend: Friend
@Deprecated("Use asFriend instead", ReplaceWith("asFriend")) @Deprecated("Use asFriend instead", ReplaceWith("asFriend"))
@PlannedRemoval("2.0-M2") @PlannedRemoval("2.0-M2")
public inline val selfQQ: Friend public val selfQQ: Friend
get() = asFriend get() = asFriend
/** /**
* 好友列表. 与服务器同步更新. * 好友列表. 与服务器同步更新.
*/ */
public abstract val friends: ContactList<Friend> public val friends: ContactList<Friend>
/** /**
* [对方 QQ 号码][id] 获取一个好友对象, 在获取失败时返回 `null`. * [对方 QQ 号码][id] 获取一个好友对象, 在获取失败时返回 `null`.
@ -155,7 +103,7 @@ public abstract class Bot internal constructor(
/** /**
* 加入的群列表. 与服务器同步更新. * 加入的群列表. 与服务器同步更新.
*/ */
public abstract val groups: ContactList<Group> public val groups: ContactList<Group>
/** /**
* [群号码][id] 获取一个群对象, 在获取失败时返回 `null`. * [群号码][id] 获取一个群对象, 在获取失败时返回 `null`.
@ -180,7 +128,7 @@ public abstract class Bot internal constructor(
* @see alsoLogin `.apply { login() }` 捷径 * @see alsoLogin `.apply { login() }` 捷径
*/ */
@JvmBlockingBridge @JvmBlockingBridge
public abstract suspend fun login() public suspend fun login()
/** /**
* 创建一个 "戳一戳" 消息 * 创建一个 "戳一戳" 消息
@ -200,44 +148,149 @@ public abstract class Bot internal constructor(
* *
* @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭 * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭
*/ */
public abstract fun close(cause: Throwable? = null) public fun close(cause: Throwable? = null)
public final override fun toString(): String = "Bot($id)"
public companion object {
@Suppress("ObjectPropertyName")
internal val _instances: WeakHashMap<Long, Bot> = WeakHashMap()
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
public val instances: List<Bot>
get() = _instances.values.filterNotNull()
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
public val instancesSequence: Sequence<Bot>
get() = _instances.values.asSequence().filterNotNull()
/**
* 获取一个 [Bot] 实例, 无对应实例时抛出 [NoSuchElementException]
*/
@JvmStatic
@Throws(NoSuchElementException::class)
public fun getInstance(qq: Long): Bot =
findInstance(qq) ?: throw NoSuchElementException(qq.toString())
/**
* 获取一个 [Bot] 实例, 无对应实例时返回 `null`
*/
@JvmStatic
public inline fun getInstanceOrNull(qq: Long): Bot? = findInstance(qq)
/**
* 获取一个 [Bot] 实例, 无对应实例时返回 `null`
*/
@JvmStatic
public fun findInstance(qq: Long): Bot? = _instances[qq]
// deprecated
/**
* 遍历每一个 [Bot] 实例
*/
@Deprecated(
"""
In Kotlin, use Sequence.forEach on instancesSequence.
In Java, use List.forEach on instances
""",
ReplaceWith("instancesSequence.forEach(block)", "net.mamoe.mirai.Bot.Companion.instancesSequence"),
DeprecationLevel.ERROR
)
@PlannedRemoval("2.0-M2")
public fun forEachInstance(block: (Bot) -> Unit): Unit = instancesSequence.forEach(block)
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
@Deprecated(
"Use instances for shorter name.",
ReplaceWith("instances", "net.mamoe.mirai.Bot.Companion.instances"),
DeprecationLevel.ERROR
)
@PlannedRemoval("2.0-M2")
public val botInstances: List<Bot>
get() = instances
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
@Deprecated(
"Use instancesSequence for shorter name.",
ReplaceWith("instancesSequence", "net.mamoe.mirai.Bot.Companion.instancesSequence"),
DeprecationLevel.ERROR
)
@PlannedRemoval("2.0-M2")
public val botInstancesSequence: Sequence<Bot>
get() = instancesSequence
}
/**
* 挂起协程直到 [Bot] 协程被关闭 ([Bot.close]).
* 即使 [Bot] 离线, 也会等待直到协程关闭.
*/
@JvmBlockingBridge
public suspend fun join(): Unit = supervisorJob.join()
/**
* 关闭这个 [Bot], 停止一切相关活动. 所有引用都会被释放.
*
* : 不可重新登录. 必须重新实例化一个 [Bot].
*
* @param cause 原因. null 时视为正常关闭, null 时视为异常关闭
*/
@JvmBlockingBridge
public suspend fun closeAndJoin(cause: Throwable? = null) {
close(cause)
join()
}
} }
/** /**
* 获取 [Job] 的协程 [Job]. [Job] 为一个 [SupervisorJob] * 获取 [Job] 的协程 [Job]. [Job] 为一个 [SupervisorJob]
*/ */
@get:JvmSynthetic @get:JvmSynthetic
public val Bot.supervisorJob: CompletableJob public inline val Bot.supervisorJob: CompletableJob
get() = this.coroutineContext[Job] as CompletableJob get() = this.coroutineContext[Job] as CompletableJob
/** /**
* 挂起协程直到 [Bot] 协程被关闭 ([Bot.close]). * [Bot] 拥有 [Friend.id] [id] 的好友时返回 `true`.
* 即使 [Bot] 离线, 也会等待直到协程关闭.
*/ */
@JvmSynthetic @JvmSynthetic
public suspend inline fun Bot.join(): Unit = this.coroutineContext[Job]!!.join() public inline fun Bot.containsFriend(id: Long): Boolean = this.friends.contains(id)
/** /**
* 关闭这个 [Bot], 停止一切相关活动. 所有引用都会被释放. * [Bot] 拥有 [Group.id] [id] 的群时返回 `true`.
*
* : 不可重新登录. 必须重新实例化一个 [Bot].
*
* @param cause 原因. null 时视为正常关闭, null 时视为异常关闭
*/ */
@JvmSynthetic @JvmSynthetic
public inline fun Bot.containsGroup(id: Long): Boolean = this.groups.contains(id)
// deprecated
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // source compatibility
@PlannedRemoval("2.0-M2")
@JvmSynthetic
public suspend inline fun Bot.join(): Unit = join()
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // source compatibility
@PlannedRemoval("2.0-M2")
@JvmSynthetic
public suspend inline fun Bot.closeAndJoin(cause: Throwable? = null) { public suspend inline fun Bot.closeAndJoin(cause: Throwable? = null) {
close(cause) close(cause)
coroutineContext[Job]?.join() coroutineContext[Job]?.join()
} }
@JvmSynthetic
public inline fun Bot.containsFriend(id: Long): Boolean = this.friends.contains(id)
@JvmSynthetic
public inline fun Bot.containsGroup(id: Long): Boolean = this.groups.contains(id)
@Deprecated("Use getFriend", ReplaceWith("this.getFriend(id)")) @Deprecated("Use getFriend", ReplaceWith("this.getFriend(id)"))
@PlannedRemoval("2.0-M2") @PlannedRemoval("2.0-M2")
@JvmSynthetic @JvmSynthetic

View File

@ -142,7 +142,7 @@ public interface Image : Message, MessageContent, CodableMessage {
@JvmStatic @JvmStatic
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun Image.queryUrl(): String { public suspend fun Image.queryUrl(): String {
val bot = Bot._instances.peekFirst()?.get() ?: error("No Bot available to query image url") val bot = Bot.instancesSequence.firstOrNull() ?: error("No Bot available to query image url")
return Mirai.queryImageUrl(bot, this) return Mirai.queryImageUrl(bot, this)
} }

View File

@ -36,11 +36,32 @@ import kotlin.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTime import kotlin.time.measureTime
internal abstract class BotImpl<N : BotNetworkHandler> constructor( internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
configuration: BotConfiguration final override val configuration: BotConfiguration,
) : Bot(configuration), CoroutineScope { final override val id: Long,
) : Bot, CoroutineScope {
// FASTEST INIT
init {
Bot._instances[this.id] = this
supervisorJob.invokeOnCompletion {
Bot._instances.remove(id)
}
}
final override val logger: MiraiLogger by lazy { configuration.botLoggerSupplier(this) } final override val logger: MiraiLogger by lazy { configuration.botLoggerSupplier(this) }
final override val coroutineContext: CoroutineContext = // for id
configuration.parentCoroutineContext
.plus(SupervisorJob(configuration.parentCoroutineContext[Job]))
.plus(configuration.parentCoroutineContext[CoroutineExceptionHandler]
?: CoroutineExceptionHandler { _, e ->
logger.error("An exception was thrown under a coroutine of Bot", e)
}
)
.plus(CoroutineName("Mirai Bot"))
// region network // region network
val network: N get() = _network val network: N get() = _network
@ -60,8 +81,8 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
@Suppress("unused") @Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> = private val offlineListener: Listener<BotOfflineEvent> =
this@BotImpl.subscribeAlways(concurrency = Listener.ConcurrencyKind.LOCKED) { event -> this@AbstractBot.subscribeAlways(concurrency = Listener.ConcurrencyKind.LOCKED) { event ->
if (event.bot != this@BotImpl) { if (event.bot != this@AbstractBot) {
return@subscribeAlways return@subscribeAlways
} }
if (!event.bot.isActive) { if (!event.bot.isActive) {
@ -107,7 +128,7 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
} }
network.withConnectionLock { network.withConnectionLock {
/** /**
* [BotImpl.relogin] only, no [BotNetworkHandler.init] * [AbstractBot.relogin] only, no [BotNetworkHandler.init]
*/ */
@OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class) @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
relogin((event as? BotOfflineEvent.Dropped)?.cause) relogin((event as? BotOfflineEvent.Dropped)?.cause)
@ -161,7 +182,7 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
/** /**
* **Exposed public API** * **Exposed public API**
* [BotImpl.relogin] && [BotNetworkHandler.init] * [AbstractBot.relogin] && [BotNetworkHandler.init]
*/ */
final override suspend fun login() { final override suspend fun login() {
@ThisApiMustBeUsedInWithConnectionLockBlock @ThisApiMustBeUsedInWithConnectionLockBlock
@ -266,7 +287,7 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
return return
} }
GlobalScope.launch { GlobalScope.launch {
runCatching { BotOfflineEvent.Active(this@BotImpl, cause).broadcast() }.exceptionOrNull() runCatching { BotOfflineEvent.Active(this@AbstractBot, cause).broadcast() }.exceptionOrNull()
?.let { logger.error(it) } ?.let { logger.error(it) }
} }
@ -278,6 +299,8 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
} }
} }
} }
final override fun toString(): String = "Bot($id)"
} }
@RequiresOptIn(level = RequiresOptIn.Level.ERROR) @RequiresOptIn(level = RequiresOptIn.Level.ERROR)

View File

@ -42,25 +42,16 @@ internal fun Bot.asQQAndroidBot(): QQAndroidBot {
internal class QQAndroidBot constructor( internal class QQAndroidBot constructor(
account: BotAccount, account: BotAccount,
configuration: BotConfiguration configuration: BotConfiguration
) : QQAndroidBotBase(account, configuration) ) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
internal abstract class QQAndroidBotBase constructor(
private val account: BotAccount,
configuration: BotConfiguration
) : BotImpl<QQAndroidBotNetworkHandler>(configuration) {
@Suppress("LeakingThis") @Suppress("LeakingThis")
val client: QQAndroidClient = val client: QQAndroidClient =
QQAndroidClient( QQAndroidClient(
account, account,
bot = this as QQAndroidBot, bot = this,
device = configuration.deviceInfo?.invoke(this) ?: DeviceInfo.random() device = configuration.deviceInfo?.invoke(this) ?: DeviceInfo.random()
) )
internal var firstLoginSucceed: Boolean = false internal var firstLoginSucceed: Boolean = false
override val id: Long
get() = account.id
inline val json get() = configuration.json inline val json get() = configuration.json
override val friends: ContactList<Friend> = ContactList() override val friends: ContactList<Friend> = ContactList()
@ -78,12 +69,14 @@ internal abstract class QQAndroidBotBase constructor(
override val asFriend: Friend by lazy { override val asFriend: Friend by lazy {
@OptIn(LowLevelApi::class) @OptIn(LowLevelApi::class)
Mirai._lowLevelNewFriend(this, object : FriendInfo { Mirai._lowLevelNewFriend(this, object : FriendInfo {
override val uin: Long get() = this@QQAndroidBotBase.id override val uin: Long get() = this@QQAndroidBot.id
override val nick: String get() = this@QQAndroidBotBase.nick override val nick: String get() = this@QQAndroidBot.nick
override val remark: String get() = "" override val remark: String get() = ""
}) })
} }
override val groups: ContactList<Group> = ContactList()
/** /**
* Final process for 'login' * Final process for 'login'
*/ */
@ -96,11 +89,9 @@ internal abstract class QQAndroidBotBase constructor(
} }
override fun createNetworkHandler(coroutineContext: CoroutineContext): QQAndroidBotNetworkHandler { override fun createNetworkHandler(coroutineContext: CoroutineContext): QQAndroidBotNetworkHandler {
return QQAndroidBotNetworkHandler(coroutineContext, this as QQAndroidBot) return QQAndroidBotNetworkHandler(coroutineContext, this)
} }
override val groups: ContactList<Group> = ContactList()
@JvmField @JvmField
val groupListModifyLock = Mutex() val groupListModifyLock = Mutex()