# Mirai - Events ## 目录 - [事件系统](#事件系统) - [快速指导](#快速指导) - [事件通道](#事件通道) - [通道操作](#通道操作) - [过滤](#过滤) - [添加 `CoroutineContext`](#添加-coroutinecontext) - [限制作用域](#限制作用域) - [链式调用](#链式调用) - [在 `EventChannel` 监听事件](#在-eventchannel-监听事件) - [监听事件的其他方法](#监听事件的其他方法) - [使用 `ListenerHost` 监听事件](#使用-eventhandler-注解标注的方法监听事件) - [在 Kotlin 使用 DSL 监听事件](#在-kotlin-使用-dsl-监听事件) - [实现事件](#实现事件) - [工具函数(Kotlin)](#工具函数kotlin) - [线性同步(`syncFromEvent`)](#线性同步syncfromevent) - [线性同步(`nextEvent`)](#线性同步nextevent) - [条件选择(`selectMessages`)](#条件选择selectmessages) - [循环条件选择(`whileSelectMessages`)](#循环条件选择whileselectmessages) ## 事件系统 Mirai 以事件驱动。 [`Event`]: ../mirai-core-api/src/commonMain/kotlin/event/Event.kt#L21-L62 每个事件都实现接口 [`Event`],且继承 `AbstractEvent`。 实现 `CancellableEvent` 的事件可以被取消(`CancellableEvent.cancel`)。 **[事件列表](../mirai-core-api/src/commonMain/kotlin/event/events/README.md#事件)** > 回到 [目录](#目录) ## 快速指导 如果你了解事件且不希望详细阅读,可以立即仿照下面示例创建事件监听并跳过本章节。 Kotlin ```kotlin // 事件监听器是协程任务。如果你有 CoroutineScope,可从 scope 继承生命周期管理和 coroutineContext GlobalEventChannel.parentScope(coroutineScope).subscribeAlways { event -> // this: GroupMessageEvent // event: GroupMessageEvent subject.sendMessage("Hello!") } // 如果不想限制生命周期,可获取 listener 处理 val listener: CompletableJob = GlobalEventChannel.subscribeAlways { event -> } listener.complete() // 停止监听 ``` Java ```java // 创建监听 Listener listener = GlobalEventChannel.INSTANCE.subscribeAlways(GroupMessageEvent.class, event -> { event.getSubject().sendMessage("Hello!"); }) listener.complete(); // 停止监听 ``` 异常默认会被相关 Bot 日志记录。可以在 `subscribeAlways` 添加如下内容来处理异常。 ``` // Kotlin .exceptionHandler { e -> e.printStackTrace() } // Java .exceptionHandler(e -> e.printStackTrace()) ``` **`GlobalEventChannel` 会监听到来自所有 `Bot` 的事件,如果只希望监听某一个,请使用 `bot.eventChannel`。** > 现在你可以继续阅读,或跳到下一章 [Messages](Messages.md) > > 回到 [目录](#目录) > [回到 Mirai 文档索引](README.md#mirai-core-api-文档) ## 事件通道 [`EventChannel`]: ../mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt [事件通道][`EventChannel`]是监听事件的入口。 **在不同的事件通道中可以监听到不同类型的事件**。 ### 获取事件通道 [`GlobalEventChannel`]: ../mirai-core-api/src/commonMain/kotlin/event/GlobalEventChannel.kt [`GlobalEventChannel`] 是最大的通道:所有的事件都可以在 [`GlobalEventChannel`] 监听到。**因此,[`GlobalEventChannel`] 会包含来自所有 `Bot` 实例的事件。** 通常不会直接使用 [`GlobalEventChannel`],而是使用经过 [通道操作](#通道操作) 操作的子通道。 > 回到 [目录](#目录) ## 通道操作 `EventChannel` 可以通过一些操作转换。 **一个通道的属性都是*不变的*:每个转换操作都会创建一个新的通道而不会修改原通道。** ### 过滤 `GlobalEventChannel` 包含任何 `Event`,可以通过 `EventChannel.filter` 过滤得到一个只包含期望的事件的 `EventChannel`。 ```kotlin var channel = GlobalEventChannel.filter { it is BotEvent && it.bot.id == 123456L } // 筛选来自某一个 Bot 的事件 ``` ```java EventChannel channel = GlobalEventChannel.INSTANCE.filter(ev -> ev instanceof BotEvent && ((BotEvent) ev).bot.id == 123456); // 筛选来自某一个 Bot 的事件 ``` > 回到 [通道操作](#通道操作) > 你可以选择跳过下文介绍的协程属性和作用域,直接阅读 [在 `EventChannel` 监听事件](#在-eventchannel-监听事件) ### 添加 `CoroutineContext` 一个通道持有属性 `defaultCoroutineContext`,将会自动添加给每个事件监听器(见后文)。 可以为通道添加一些 `CoroutineContext`,如 `CoroutineExceptionHandler`(用于处理监听时产生的异常)。 ```kotlin channel.exceptionHandler { exception -> logger.error(exception) } ``` ```java channel.exceptionHandler(exception -> logger.error(exception); ); ``` 这本质上是添加了一个 `CoroutineExceptionHandler`。之后当事件监听器出现异常,异常就会被传递到这个 `CoroutineExceptionHandler` 处理。 > 回到 [通道操作](#通道操作) ### 限制作用域 在监听时会创建一些*事件监听器*。*事件监听器*本质上是一个协程 *`Job`*,因此可以有父 `Job`。 要指定父 `Job`,请使用 `parentJob` 或 `parentScope` 操作。 ```kotlin // parentScope channel = channel.parentScope(MyApplicationScope) // parentJob val job = SupervisorJob() channel = channel.parentJob(job) ``` 在 Kotlin,可以使用如下扩展快速在 GlobalEventChannel 创建一个指定协程作用域下的事件通道。 > ```kotlin > fun CoroutineScope.globalEventChannel(coroutineContext: CoroutineContext = EmptyCoroutineContext): EventChannel = GlobalEventChannel.parentScope(this, coroutineContext) > ``` ```kotlin val channel = MyApplicationScope.globalEventChannel() ``` 作用域限制对于应用生命周期管理会很有用。请看如下 Mirai Console 插件示例。 ```kotlin object MyPluginMain : KotlinPlugin() { // KotlinPlugin 实现了 CoroutineScope // 插件被启用时调用 override fun onEnable() { // `this` 是插件的协程作用域 // 在插件协程作用域里创建事件监听。当插件被停用时,插件的协程作用域也会被关闭,事件监听就会被同步停止。 this.globalEventChannel().subscribeAlways { event -> // 处理事件 } } } ``` > 有关限制作用域的实现细节,可在使用时阅读源码内文档。 ### 链式调用 对通道的操作都会返回 `this`,因此可以链式调用。 ```kotlin val channel = GlobalEventChannel .filterIsInstance() .filter { it.bot.id == 123456L } .filter { /* some other conditions */ } .parentScope(MyApplicationScope) .exceptionHandler { exception -> exception.printStacktrace() } ``` > 回到 [通道操作](#通道操作) > 回到 [目录](#目录) ## 在 `EventChannel` 监听事件 使用: - `EventChannel.subscribe`:监听事件并自行觉得何时停止 - `EventChannel.subscribeAlways`:一直监听事件 - `EventChannel.subscribeOnce`:只监听一次事件 ```kotlin bot.eventChannel.subscribeAlways { event -> // this: GroupMessageEvent // event: GroupMessageEvent subject.sendMessage("Hello from mirai!") } ``` ```java bot.eventChannel.subscribeAlways(GroupMessageEvent.class, event -> { event.getSubject().sendMessage("Hello from mirai!"); }) ``` > 实现细节可查看源码内注释。 > 回到 [目录](#目录) ## 监听事件的其他方法 监听都需要在*事件通道*中进行。如下几种方法都本质上会调用上述 `EventChannel.subscribe` 等方法。 - [使用 `@EventHandler` 注解标注的方法监听事件](#使用-eventhandler-注解标注的方法监听事件) - [在 Kotlin 使用 DSL 监听事件](#在-kotlin-使用-dsl-监听事件) ### 使用 `@EventHandler` 注解标注的方法监听事件 标注一个函数(方法)为事件监听器。mirai 通过反射获取他们并为之注册事件。 - [Kotlin 函数](#kotlin-函数) - [Java 方法](#java-方法) #### Kotlin 函数 Kotlin 函数要求: - 接收者和函数参数: 所标注的 Kotlin 函数必须至少拥有一个接收者或一个函数参数, 或二者都具有. 接收者和函数参数的类型必须相同 (如果二者都存在) 接收者或函数参数的类型都必须为 `Event` 或其子类. 所有 Kotlin 非 `suspend` 的函数都将会在 `Dispatchers.IO` 中调用 支持的函数类型: ```kotlin // 所有函数参数, 函数返回值都不允许标记为可空 (带有 '?' 符号) // T 表示任何 Event 类型. suspend fun T.onEvent(T) suspend fun T.onEvent(T): ListeningStatus suspend fun T.onEvent(T): Nothing suspend fun onEvent(T) suspend fun onEvent(T): ListeningStatus suspend fun onEvent(T): Nothing suspend fun T.onEvent() suspend fun T.onEvent(): ListeningStatus suspend fun T.onEvent(): Nothing fun T.onEvent(T) fun T.onEvent(T): ListeningStatus fun T.onEvent(T): Nothing fun onEvent(T) fun onEvent(T): ListeningStatus fun onEvent(T): Nothing fun T.onEvent() fun T.onEvent(): ListeningStatus fun T.onEvent(): Nothing ``` Kotlin 使用示例: - 独立 `CoroutineScope` 和 `ListenerHost` ```kotlin object MyEvents : ListenerHost { override val coroutineContext = SupervisorJob() // 可以抛出任何异常, 将在 this.coroutineContext 或 registerEvents 时提供的 CoroutineScope.coroutineContext 中的 CoroutineExceptionHandler 处理. @EventHandler suspend fun MessageEvent.onMessage() { reply("received") } } eventChannel.registerListenerHost(MyEvents) ``` `onMessage` 抛出的异常将会交给 `myCoroutineScope` 处理 - 合并 `CoroutineScope` 和 `ListenerHost`: 使用 `SimpleListenerHost` ```kotlin object MyEvents : SimpleListenerHost( /* override coroutineContext here */ ) { override fun handleException(context: CoroutineContext, exception: Throwable) { // 处理 onMessage 中未捕获的异常 } @EventHandler suspend fun MessageEvent.onMessage() { // 可以抛出任何异常, 将在 handleException 处理 reply("received") // 无返回值 (或者返回 Unit), 表示一直监听事件. } @EventHandler suspend fun MessageEvent.onMessage(): ListeningStatus { // 可以抛出任何异常, 将在 handleException 处理 reply("received") return ListeningStatus.LISTENING // 表示继续监听事件 // return ListeningStatus.STOPPED // 表示停止监听事件 } } eventChannel.registerListenerHost(MyEvents) ``` #### Java 方法 所有 Java 方法都会在 `Dispatchers.IO` 中调用,因此在 Java 也可以调用阻塞方法。 支持的方法类型: ``` // T 表示任何 Event 类型. void onEvent(T) Void onEvent(T) ListeningStatus onEvent(T) // 禁止返回 null ``` Java 使用示例: ```java public class MyEventHandlers extends SimpleListenerHost { @Override public void handleException(@NotNull CoroutineContext context, @NotNull Throwable exception){ // 处理事件处理时抛出的异常 } @EventHandler public void onMessage(@NotNull MessageEvent event) throws Exception { // 可以抛出任何异常, 将在 handleException 处理 event.subject.sendMessage("received"); // 无返回值, 表示一直监听事件. } @NotNull @EventHandler public ListeningStatus onMessage(@NotNull MessageEvent event) throws Exception { // 可以抛出任何异常, 将在 handleException 处理 event.subject.sendMessage("received"); return ListeningStatus.LISTENING; // 表示继续监听事件 // return ListeningStatus.STOPPED; // 表示停止监听事件 } } // 注册: // eventChannel.registerListenerHost(new MyEventHandlers()) ``` > 回到 [监听事件的其他方法](#监听事件的其他方法) ### 在 Kotlin 使用 DSL 监听事件 > **警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题** [subscribeMessages](../mirai-core-api/src/commonMain/kotlin/event/subscribeMessages.kt#L37-L64) 示例: ```kotlin eventChannel.subscribeMessages { "test" { // 当消息内容为 "test" 时执行 // this: MessageEvent reply("test!") } "Hello" reply "Hi" // 当消息内容为 "Hello" 时回复 "Hi" "quote me" quoteReply "ok" // 当消息内容为 "quote me" 时引用该消息并回复 "ok" "quote me2" quoteReply { // lambda 也是允许的: // 返回值接受 Any? // 为 Unit 时不发送任何内容; // 为 Message 时直接发送; // 为 String 时发送为 PlainText; // 否则 toString 并发送为 PlainText "ok" } case("iGNorECase", ignoreCase=true) reply "OK" // 忽略大小写 startsWith("-") reply { cmd -> // 当消息内容以 "-" 开头时执行 // cmd 为消息去除开头 "-" 的内容 } val listener: Listener = "1" reply "2" // 每个语句都会被注册为事件监听器,可以这样获取监听器 listener.complete() // 停止 "1" reply "2" 这个事件监听 } ``` > 回到 [目录](#目录) ## 实现事件 只要实现接口 `Event` 并继承 `AbstractEvent` 的对象就可以被广播。 要广播一个事件,使用 `Event.broadcast()`(Kotlin)或 `EventKt.broadcast(Event)`(Java)。 > 回到 [目录](#目录) ## 工具函数(Kotlin) > *可能需要较好的 Kotlin 技能才能理解以下内容。* > **可以[跳过本节](# )** 基于 Kotlin 协程特性,mirai 提供 ` ### 线性同步(`syncFromEvent`) [syncFromEvent.kt](../mirai-core-api/src/commonMain/kotlin/event/syncFromEvent.kt) 挂起协程并获取下一个戳 Bot 的对象: ```kotlin val target: UserOrBot = syncFromEvent { sender } ``` 带超时版本: ```kotlin val target: UserOrBot = syncFromEvent(5000) { sender } // 5000ms ``` 异步 `async` 版本: ```kotlin val target: Deferred = coroutineScope.asyncFromEvent { sender } ``` ### 线性同步(`nextEvent`) [nextEvent.kt](../mirai-core-api/src/commonMain/kotlin/event/nextEvent.kt) 挂起协程并获取下一个指定事件: ```kotlin val event: BotNudgedEvent = nextEvent() ``` 带超时和过滤器版本: ```kotlin val event: BotNudgedEvent = nextEvent(5000) { it.bot.id == 123456L } ``` ### 条件选择(`selectMessages`) > **警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题** [select.kt](../mirai-core-api/src/commonMain/kotlin/event/select.kt) 类似于 Kotlin 协程 `select`,mirai 也提供类似的功能。 `selectMessages`:挂起当前协程,等待任意一个事件监听器触发后返回其返回值。 ```kotlin MyCoroutineScope.subscribeAlways { if (message.contentEquals("ocr")) { reply("请发送你要进行 OCR 的图片或图片链接") val image: InputStream = selectMessages { has { URL(it.queryUrl()).openStream() } has { URL(it.content).openStream() } defaultReply { "请发送图片或图片链接" } timeout(30_000) { event.quoteReply("请在 30 秒内发送图片或图片链接"); null } } ?: return@subscribeAlways val result = ocr(image) quoteReply(result) } } ``` 这种语法就相当于(伪代码): ``` val image = when (下一条消息) { 包含图片 { 查询图片链接() } 包含纯文本 { 下载图片() } 其他情况 { 引用回复() } 超时 { 引用回复() } } ``` ### 循环条件选择(`whileSelectMessages`) > **警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题** [select.kt](../mirai-core-api/src/commonMain/kotlin/event/select.kt) 类似于 Kotlin 协程 `whileSelect`,mirai 也提供类似的功能。 `whileSelectMessages`:挂起当前协程,等待任意一个事件监听器返回 `false` 后返回。 ```kotlin reply("开启复读模式") whileSelectMessages { "stop" { reply("已关闭复读") false // 停止循环 } // 也可以使用 startsWith("") { true } 等 DSL default { reply(message) true // 继续循环 } timeout(3000) { // on true } } // 等待直到 `false` reply("复读模式结束") ``` > 回到 [目录](#目录) ######   ---- > 下一步,[Messages](Messages.md) > > [回到 Mirai 文档索引](README.md#mirai-core-api-文档)