Mirai - Events

目录

事件系统

Mirai 许多功能都依赖事件。

每个事件都实现接口 Eventopen in new window,且继承 AbstractEvent
实现 CancellableEvent 的事件可以被取消(CancellableEvent.cancel)。

事件列表

回到 目录

快速指导

如果你了解事件且不希望详细阅读,可以立即仿照下面示例创建事件监听并跳过本章节。

注意,GlobalEventChannel 会监听到来自所有 Bot 的事件,如果只希望监听某一个 Bot 的事件,请使用 bot.eventChannel

有关消息 MessageMessageChain 将会在后文 消息系统 章节解释。

Kotlin

// 事件监听器是协程任务。如果你有 CoroutineScope,可从 scope 继承生命周期管理和 coroutineContext
GlobalEventChannel.parentScope(coroutineScope).subscribeAlways<GroupMessageEvent> { event ->
    // this: GroupMessageEvent
    // event: GroupMessageEvent
    
    // `event.message` 是接收到的消息内容, 可自行处理. 由于 `this` 也是 `GroupMessageEvent`, 可以通过 `message` 直接获取. 详细查阅 `GroupMessageEvent`.
    
    subject.sendMessage("Hello!")
}
// `GlobalEventChannel.parentScope(coroutineScope)` 也可以替换为使用扩展 `coroutineScope.globalEventChannel()`, 根据个人习惯选择



// 如果不想限制生命周期,可获取 listener 处理
val listener: CompletableJob =
    GlobalEventChannel.subscribeAlways<GroupMessageEvent> { event -> }

listener.complete() // 停止监听

异常默认会被相关 Bot 日志记录。可以在 subscribeAlways 之前添加如下内容来处理异常。

.exceptionHandler { e -> e.printStackTrace() }

注意:如果要在 Mirai Console 插件中监听事件,请不要使用使用无作用域控制的 GlobalEventChannel ,如 GlobalEventChannel.subscribeAlways 。请使用插件主类的扩展函数 globalEventChannel() 或者 GlobalEventChannel.parentScope(scope) 等方式控制监听器协程作用域。

Java

// 创建监听
Listener listener=GlobalEventChannel.INSTANCE.parentScope(scope).subscribeAlways(GroupMessageEvent.class,event->{
        MessageChain chain=event.getMessage(); // 可获取到消息内容等, 详细查阅 `GroupMessageEvent`

        event.getSubject().sendMessage("Hello!"); // 回复消息
        })

        listener.complete(); // 停止监听 

异常默认会被相关 Bot 日志记录。可以在 subscribeAlways 之前添加如下内容来处理异常。

.exceptionHandler(e->e.printStackTrace())

注意:如果要在 Mirai Console 插件中监听事件,请不要使用使用无作用域控制的 GlobalEventChannel ,如 GlobalEventChannel.subscribeAlways 。请使用 GlobalEventChannel.parentScope(PluginMain.INSTANCE) 等方式控制监听器协程作用域。

你已经了解了基本事件操作。现在你可以继续阅读通道处理和扩展等内容,或:

事件通道

事件通道open in new window是监听事件的入口。 在不同的事件通道中可以监听到不同类型的事件

获取事件通道

GlobalEventChannelopen in new window 是最大的通道:所有的事件都可以在 GlobalEventChannelopen in new window 监听到。因此,GlobalEventChannelopen in new window 会包含来自所有 Bot 实例的事件。

通常不会直接使用 GlobalEventChannelopen in new window,而是使用经过 通道操作 操作的子通道。

回到 目录

通道操作

EventChannel 可以通过一些操作转换。

一个通道的属性都是不变的:每个转换操作都会创建一个新的通道而不会修改原通道。

过滤

GlobalEventChannel 包含任何 Event,可以通过 EventChannel.filter 过滤得到一个只包含期望的事件的 EventChannel

var channel = GlobalEventChannel.filter { it is BotEvent && it.bot.id == 123456L } // 筛选来自某一个 Bot 的事件
EventChannel channel = GlobalEventChannel.INSTANCE.filter(ev -> ev instanceof BotEvent && ((BotEvent) ev).bot.id == 123456); // 筛选来自某一个 Bot 的事件

回到 通道操作

你可以选择跳过下文介绍的协程属性和作用域,直接阅读 EventChannel 监听事件

添加 CoroutineContext

一个通道持有属性 defaultCoroutineContext,将会自动添加给每个事件监听器(见后文)。

可以为通道添加一些 CoroutineContext,如 CoroutineExceptionHandler(用于处理监听时产生的异常)。

channel.exceptionHandler { exception ->
    logger.error(exception)
} 
channel.exceptionHandler(exception ->
    logger.error(exception);
);

这本质上是添加了一个 CoroutineExceptionHandler。之后当事件监听器出现异常,异常就会被传递到这个 CoroutineExceptionHandler 处理。

回到 通道操作

限制作用域

在监听时会创建一些事件监听器事件监听器本质上是一个协程 Job,因此可以有父 Job

要指定父 Job,请使用 parentJobparentScope 操作。

// parentScope
channel = channel.parentScope(MyApplicationScope)

// parentJob
val job = SupervisorJob()
channel = channel.parentJob(job)

在 Kotlin,可以使用如下扩展快速在 GlobalEventChannel 创建一个指定协程作用域下的事件通道。

 fun CoroutineScope.globalEventChannel(coroutineContext: CoroutineContext = EmptyCoroutineContext): EventChannel<Event> = GlobalEventChannel.parentScope(this, coroutineContext)
val channel = MyApplicationScope.globalEventChannel()

作用域限制对于应用生命周期管理会很有用。请看如下 Mirai Console 插件示例。

object MyPluginMain : KotlinPlugin() { // KotlinPlugin 实现了 CoroutineScope

    // 插件被启用时调用
    override fun onEnable() {
    
        // `this` 是插件的协程作用域
        // 在插件协程作用域里创建事件监听。当插件被停用时,插件的协程作用域也会被关闭,事件监听就会被同步停止。
        this.globalEventChannel().subscribeAlways<MessageEvent> { event ->
            // 处理事件 
        }
    }
}

有关限制作用域的实现细节,可在使用时阅读源码内文档。

链式调用

对通道的操作都会返回 this,因此可以链式调用。

val channel = GlobalEventChannel
    .filterIsInstance<BotEvent>()
    .filter { it.bot.id == 123456L }
    .filter { /* some other conditions */ }
    .parentScope(MyApplicationScope)
    .exceptionHandler { exception ->
        exception.printStacktrace()
    }

回到 通道操作 回到 目录

EventChannel 监听事件

使用:

  • EventChannel.subscribe:监听事件并自行决定何时停止
  • EventChannel.subscribeAlways:一直监听事件
  • EventChannel.subscribeOnce:只监听一次事件
bot.eventChannel.subscribeAlways<GroupMessageEvent> { event ->
    // this: GroupMessageEvent
    // event: GroupMessageEvent
    
    subject.sendMessage("Hello from mirai!")
}
bot.eventChannel.subscribeAlways(GroupMessageEvent.class, event -> {
    event.getSubject().sendMessage("Hello from mirai!");
})

实现细节可查看源码内注释。

回到 目录

监听事件的其他方法

监听都需要在事件通道中进行。如下几种方法都本质上会调用上述 EventChannel.subscribe 等方法。

使用 @EventHandler 注解标注的方法监听事件

标注一个函数(方法)为事件监听器。mirai 通过反射获取他们并为之注册事件。

Kotlin 函数

Kotlin 函数要求:

  • 接收者和函数参数: 所标注的 Kotlin 函数必须至少拥有一个接收者或一个函数参数, 或二者都具有. 接收者和函数参数的类型必须相同 (如果二者都存在) 接收者或函数参数的类型都必须为 Event 或其子类.

所有 Kotlin 非 suspend 的函数都将会在 Dispatchers.IO 中调用

支持的函数类型:

// 所有函数参数, 函数返回值都不允许标记为可空 (带有 '?' 符号)
// 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 使用示例:

  • 独立 CoroutineScopeListenerHost
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 处理

  • 合并 CoroutineScopeListenerHost: 使用 SimpleListenerHost
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 使用示例:

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.getSubject().sendMessage("received");
        // 无返回值, 表示一直监听事件.
    }
    @NotNull
    @EventHandler
    public ListeningStatus onMessage(@NotNull MessageEvent event) throws Exception { // 可以抛出任何异常, 将在 handleException 处理
        event.getSubject().sendMessage("received");
        return ListeningStatus.LISTENING; // 表示继续监听事件
        // return ListeningStatus.STOPPED; // 表示停止监听事件
    }
}

// 注册:
// eventChannel.registerListenerHost(new MyEventHandlers())

回到 监听事件的其他方法

在 Kotlin 使用 DSL 监听事件

警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题

subscribeMessagesopen in new window

示例:

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<MessageEvent> = "1" reply "2"
    // 每个语句都会被注册为事件监听器,可以这样获取监听器
    
    listener.complete() // 停止 "1" reply "2" 这个事件监听
}

回到 目录

实现事件

相信你在使用 mirai 自带的事件时已经感到受益匪浅了,这种机制也可以作用在你的程序上,让其他人的程序也能像监听 mirai 自带的事件一样,对你程序的行为作出反应。

新建事件

新建一个类,让类实现接口 Event 并继承 AbstractEvent 即可。根据内部实现,事件必须继承 AbstractEvent,如下示例。

// kotlin:
class ExampleEvent(
    var action: String
) : AbstractEvent()
// java:
public class ExampleEvent extends AbstractEvent {
    String action;
    public ExampleEvent(String action) {
        this.action = action;
    }
    public String getAction() {
        return action;
    }
    public void setAction(String action) {
        this.action = action;
    }
}

广播自定义事件

事件需要被广播,才会被监听器接收到,使已监听事件的程序作出响应。以上文的 ExampleEvent 为例,

kotlin:event.broadcast()

java:EventKt.broadcast(Event)

注: 在 kotlin 中进行事件的广播需要在协程上下文执行

// kotlin:
var event = ExampleEvent("some action")
var finalAction = event.broadcast().action
println("action = $finalAction")
// java:
ExampleEvent event = new ExampleEvent("some action");
String finalAction = EventKt.broadcast(event).getAction();
System.out.println("action = " + finalAction);

监听自定义事件

同上文监听事件的方式几乎一样。不过需要注意的是,从 bot 获取的消息通道 (bot.eventChannel),只能监听 BotEvent,如果你的事件类没有实现 BotEvent,将无法通过这个通道来监听此事件。因此你可能需要使用 GlobalEventChannel 来代替 bot.eventChannel

以下的示例是 监听事件以影响上一个部分 广播自定义事件 中的变量 action 的值。

// kotlin:
GlobalEventChannel.subscribeAlways<ExampleEvent> { event ->
    // 处理事件
    if (event.action == "some action") {
        event.action = "another action"
    }
}
// java:
GlobalEventChannel.INSTANCE.subscribeAlways(ExampleEvent.class, event -> { 
    // 处理事件
    if (event.getAction().equals("some action")) {
        event.setAction("another action");
    }
});

在 Console 中自定义事件

请参考 Console - JVMPlugin - Data Exchange

回到 目录

工具函数(Kotlin)

可能需要较好的 Kotlin 技能才能理解以下内容。可以跳过本节

基于 Kotlin 协程特性,mirai 提供 `

线性同步(syncFromEvent

syncFromEvent.ktopen in new window

挂起协程并获取下一个戳 Bot 的对象:

val target: UserOrBot = syncFromEvent<BotNudgedEvent> { sender }

带超时版本:

val target: UserOrBot = syncFromEvent<BotNudgedEvent>(5000) { sender } // 5000ms

异步 async 版本:

val target: Deferred<UserOrBot> = coroutineScope.asyncFromEvent<BotNudgedEvent> { sender }

线性同步(nextEvent

nextEvent.ktopen in new window

挂起协程并获取下一个指定事件:

val event: BotNudgedEvent = nextEvent<BotNudgedEvent>()

带超时和过滤器版本:

val event: BotNudgedEvent = nextEvent<BotNudgedEvent>(5000) { it.bot.id == 123456L }

条件选择(selectMessages

警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题

select.ktopen in new window

类似于 Kotlin 协程 select,mirai 也提供类似的功能。

selectMessages:挂起当前协程,等待任意一个事件监听器触发后返回其返回值。

MyCoroutineScope.subscribeAlways<GroupMessageEvent> {
    if (message.contentEquals("ocr")) {
        subject.sendMessage("请发送你要进行 OCR 的图片或图片链接")
        val image: InputStream = selectMessages {
            has<Image> { URL(it.queryUrl()).openStream() }
            has<PlainText> { URL(it.content).openStream() }
            defaultReply { "请发送图片或图片链接" }
            timeout(30_000) { event.quoteReply("请在 30 秒内发送图片或图片链接"); null }
        } ?: return@subscribeAlways
        
        val result = ocr(image)
        subject.sendMessage(message.quote() + result)
    }
}

这种语法就相当于(伪代码):

val image = when (下一条消息) {
   包含图片 { 查询图片链接() } 
   包含纯文本URL { 下载图片() }
   其他情况 { 引用回复() }
   超时 { 引用回复() }
}

循环条件选择(whileSelectMessages

警告:此节内容需要坚实的 Kotlin 技能,盲目使用会导致问题

select.ktopen in new window

类似于 Kotlin 协程 whileSelect,mirai 也提供类似的功能。

whileSelectMessages:挂起当前协程,等待任意一个事件监听器返回 false 后返回。

subject.sendMessage("开启复读模式")
whileSelectMessages {
    "stop" {
        subject.sendMessage("已关闭复读")
        false // 停止循环
    }
    // 也可以使用 startsWith("") { ... } 等 DSL
    default {
        subject.sendMessage(message)
        true // 继续循环
    }
    timeout(3000) {
        // on
        true
    }
} // 等待直到 `false`
subject.sendMessage("复读模式结束")

回到 目录


下一步,Messages

回到 Mirai 文档索引