diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index aa734e47f..e8f4f6a32 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -33,6 +33,7 @@ import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.internal.data.castOrNull import net.mamoe.mirai.console.internal.plugin.rootCauseOrSelf import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +import net.mamoe.mirai.console.util.MessageScope import net.mamoe.mirai.console.util.childScope import net.mamoe.mirai.console.util.childScopeContext import net.mamoe.mirai.contact.* @@ -121,6 +122,11 @@ import kotlin.internal.LowPriorityInOverloadResolution * +-----------------------------+----------------------------+---------------+ * ``` * + * ## Scoping: [MessageScope] + * 在处理多个消息对象时, 可通过 [MessageScope] 简化操作. + * + * 查看 [MessageScope] 以获取更多信息. + * * @see ConsoleCommandSender 控制台 * @see UserCommandSender [User] ([群成员][Member], [好友][Friend]) * @see toCommandSender diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt index 5996ec461..cda7b83fb 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt @@ -20,26 +20,111 @@ import kotlinx.coroutines.flow.fold import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.User import net.mamoe.mirai.message.data.Message import kotlin.internal.InlineOnly import kotlin.internal.LowPriorityInOverloadResolution -@ConsoleExperimentalAPI +/** + * 表示几个消息对象的 '域', 即消息对象的集合. 用于最小化将同一条消息发送给多个类型不同的目标的付出. + * + * ## 支持的消息对象类型 + * [Contact], [CommandSender], [MessageScope] (递归). + * + * 在下文, `A` 或 `B` 指代这三种类型的其中两种, 允许排列组合. 如 `A.scopeWith(B)` 可能表示 `Contact.scopeWith(MessageScope)`. + * + * ## 获得 [MessageScope] + * - `A.asMessageScope()`. + * - `C.toMessageScope()`. 其中 `C` 表示 `Iterable`, `Sequence`, `Flow`, `Array` 其中任一. + * + * ## 连接 [MessageScope] + * - `A.scopeWith(vararg B)`. + * - `A.scopeWith(vararg A)`. + * - `A.scopeWithNotNull(vararg B?)`. 类似 [listOfNotNull]. + * - `A.scopeWithNotNull(vararg A?)`. 类似 [listOfNotNull]. + * + * ## 自动去重 + * 在连接时, [MessageScope] 会自动根据真实的 [收信对象][CommandSender.subject] 去重. + * + * 如 `member.asCommandSender().scopeWith(member.group)`, + * 返回的 [MessageScope] 实际上只包含 `member.group`. 因为 `member.asCommandSender()` 的最终收信对象就是 `member.group`. + * + * 因此在使用 [scopeWith] 时, 无需考虑重复性, 只需要把希望发送的目标全部列入. + * + * ## 使用 [MessageScope] + * 在 `scopeWith` 或 `scopeWithNotNull` 后加 `lambda` 参数即可表示使用 [MessageScope]. + * 如: + * ``` + * A.scopeWith(B) { // this: MessageScope + * sendMessage(...) + * } + * ``` + * + * ## 典例 + * 在处理指令时, 目标群对象可能与发件人群对象不同, 如用户在 A 群发指令, 以禁言 B 群的成员. + * 此时机器人可能需要同时广播通知到 A 群和 B 群. + * + * 由于 [CommandSender] 与 [Contact] 无公共接口, 无法使用 [listOfNotNull] 遍历处理. [MessageScope] 就是设计为解决这样的问题. + * + * ``` + * // 在一个 CompositeCommand 内 + * @Handler + * suspend fun CommandSender.handle(target: Member) { + * val duration = Random.nextInt(1, 15) + * target.mute(duration) + * + * // 不使用 MessageScope, 无用的样板代码 + * val thisGroup = this.getGroupOrNull() + * val message = "${this.name} 禁言 ${target.nameCardOrNick} $duration 秒" + * if (target.group != thisGroup) { + * target.group.sendMessage(message) + * } + * sendMessage(message) + * + * // 使用 MessageScope, 清晰逻辑 + * // 表示至少发送给 `this`, 当 `this` 的真实发信对象与 `target.group` 不同时, 还额外发送给 `target.group` + * this.scopeWithNotNull(target.group) { + * sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒") + * } + * + * // 同样地, 可以扩展用法, 同时私聊指令执行者: + * // this.scopeWithNotNull( + * // target, + * // target.group + * // ) { ... } + * } + * ``` + */ public interface MessageScope { /** - * 立刻发送一条消息. + * 如果此 [MessageScope], 仅包含一个消息对象, 则 [realTarget] 指向这个对象. + * + * 对于 [CommandSender] 作为 [MessageScope], [realTarget] 总是指令执行者 [User], 即 [CommandSender.user] + * + * [realTarget] 用于 [MessageScope.invoke] 时的去重. + * + * @suppress 此 API 不稳定, 可能在任何时间被修改 + */ + @ConsoleExperimentalAPI + public val realTarget: Any? + + /** + * 立刻以此发送消息给所有在此 [MessageScope] 下的消息对象 */ @JvmBlockingBridge public suspend fun sendMessage(message: Message) /** - * 立刻发送一条消息. + * 立刻以此发送消息给所有在此 [MessageScope] 下的消息对象 */ @JvmDefault @JvmBlockingBridge public suspend fun sendMessage(message: String) } +/** + * 使用 [MessageScope] 里的所有消息对象. 与 [kotlin.run] 相同. + */ @JvmSynthetic public inline operator fun MS.invoke(action: MS.() -> R): R = this.action() @@ -47,6 +132,10 @@ public inline operator fun MS.invoke(action: MS.() -> R): // Builders /////////////////////////////////////////////////////////////////////////// +/* + * 实现提示: 以下所有代码都通过 codegen 模块中 net.mamoe.mirai.console.codegen.MessageScopeCodegen 生成. 请不要手动修改它们. + */ + //// region MessageScopeBuilders CODEGEN //// public fun Contact.asMessageScope(): MessageScope = createScopeDelegate(this) @@ -531,7 +620,7 @@ public suspend fun Flow.toMessageScope(): MessageScope { // Flow { + return if (this is CombinedScope) { + sequenceOf(this.first.asSequence(), this.second.asSequence()).flatten() + } else sequenceOf(this) +} + private class CombinedScope( private val first: MessageScope, private val second: MessageScope ) : MessageScope { + override val realTarget: Any? get() = null + + private val targets: List by lazy { + this.asSequence().distinctBy { it.realTarget }.toList() + } + override suspend fun sendMessage(message: Message) { - first.sendMessage(message) - second.sendMessage(message) + for (target in targets) { + target.sendMessage(message) + } } override suspend fun sendMessage(message: String) { - first.sendMessage(message) - second.sendMessage(message) + for (target in targets) { + target.sendMessage(message) + } } } private class CommandSenderAsMessageScope( private val sender: CommandSender ) : MessageScope { + override val realTarget: Any? + get() = sender.user ?: sender // ConsoleCommandSender + override suspend fun sendMessage(message: Message) { sender.sendMessage(message) } @@ -582,6 +688,9 @@ private class CommandSenderAsMessageScope( private class ContactAsMessageScope( private val sender: Contact ) : MessageScope { + override val realTarget: Any? + get() = sender + override suspend fun sendMessage(message: Message) { sender.sendMessage(message) } @@ -592,6 +701,9 @@ private class ContactAsMessageScope( } private object NoopMessageScope : MessageScope { + override val realTarget: Any? + get() = null + override suspend fun sendMessage(message: Message) { }