跳到主要内容
版本:3.3.0

事件监听

事件监听可能是你最需要了解的功能。本章将会为你介绍如何去监听一个事件。换言之,即如何写一个监听函数。

监听函数

在了解事件监听之前,你应当已经了解过了 监听函数事件处理上下文

基础监听

对事件的监听是对于事件调度的基础。首先,我们在 Simple Application 下来聊聊事件监听的注册。

预注册

Simple Application 的构建阶段,其提供了 eventProcessor 作用域来为事件处理器 (或者说事件调度器)来提供配置,而对事件的预注册便可以在这其中完成:

simbotApplication(Simple) { 
eventProcessor {
listeners {
// 监听函数作用域

// 方式1
listen(FooEvent) {
match { true }
process {
// 事件处理逻辑...
}
}

// 方式2
FooEvent {
// 事件处理逻辑...
} onMatch {
true
}

// 通过运算符直接添加listener
val listener: EventListener = ...

+listener
+listener.toRegistrationDescription {
// ...
}
}

// 直接添加listener
addListener(...)
addListenerRegistrationDescription(...)
}
}

上述的 eventProcessor { listeners { } } 可以被简化,而省略掉外层的 eventProcessor

simbotApplication(Simple) { 
listeners {
// ...
}
}

直接注册

在这其中,通过 eventProcessoraddListener 可能是最直观的监听函数注册方式了。 就如同你猜的那样,此方法直接提供一个监听函数实例,并注册。

simbotApplication(Simple) { 
eventProcessor {

// 直接添加listener
addListener(...)
addListenerRegistrationDescription(...)
}
}

listeners

另外一种方式便是在 listeners { } 作用域中进行监听函数的构建了。

listeners 作用域中的实际接收者类型为 EventListenerRegistrationDescriptionsGenerator, 代表用于生成 EventListenerRegistrationDescription 的生成器。

使用它主要有两种方法.

方式1:

simbotApplication(Simple) {
listeners {
listen(FooEvent) {
match { ... }
process { ... }
// or handle { ... }
}
}
}

通过 listen(...) 指定一个事件类型,并通过Builder来进行配置。 由于这个生成器可以生成 EventListenerRegistrationDescription, 所以它也能配置一些额外属性:

listeners {
listen(FooEvent) {
isAsync = true
priority = PriorityConstant.FIRST

match { ... }
process { ... }
// or handle { ... }
}
}
都是一家人

这其中的规则与在监听函数中描述的一样,match 可以配置多次,而 processhandle 则必须且只能 配置一次。

方式2:

simbotApplication(Simple) {
listeners {
FooEvent { event ->
// 事件处理逻辑
} onMatch { event ->
// 事件匹配逻辑
}
}
}

这实际上可以算是上述方式1的一种...简写或扩展。上面这实例实质上是这个样子的:

simbotApplication(Simple) {
listeners {
FooEvent.invoke { event ->
// 事件处理逻辑
}.onMatch { event ->
// 事件匹配逻辑
}
}
}

invoke 通常会被省略,因此可以简化为

FooEvent { /* 事件处理逻辑 */ } /*  onMatch { 事件匹配逻辑 } */

上述示例中,我们没有在事件处理逻辑的结尾提供 EventResult 结果,因此它实际上是相当于使用了 process。 如果你希望通过这种方法的时候指定事件处理结果,你可以这样:

FooEvent.handle { event ->
// ...
EventResult.of(...)
} onMatch {
// 事件匹配逻辑
}

不使用 invoke 或者 process 而是使用 handle 扩展函数。

与方式1一样,onMatch 也支持配置多次:

FooEvent.handle { event ->
// ...
EventResult.of(...)
} onMatch {
// 事件匹配逻辑
} onMatch {
// 事件匹配逻辑
}

而至于事件处理逻辑...你或许不用担心了。

动态注册

除了我们前文一直在讲的“预注册”,在application启动后也支持动态注册监听函数。

val launcher = simbotApplication(Simple) { 
// ...
}

val application = launcher.launch()

// 注册监听函数
val handle = application.eventListenerManager.listeners {
// 方式1
listen(FooEvent) {
match { true }
process {
// 事件处理逻辑...
}
}

// 方式2
FooEvent {
// 事件处理逻辑...
} onMatch {
true
}

// 通过运算符直接添加listener
val listener: EventListener = ...

+listener
+listener.toRegistrationDescription {
// ...
}

// 直接添加listener
listener(listener)
listener(listener.toRegistrationDescription {
// ...
})
}
差不多

eventListenerManager.listeners 作用域中的API基本与 application.eventProcessor.listeners 中的API一致。

注解监听

看到这里,你可能会想:“这跟宣传的不一样啊!不是加个 @Listener 注解就能用了吗?” 或者 “这在Java中也太麻烦了吧!” 之类的想法。同样也是为了解决这个问题,我们提供了一个叫做 BOOT 的模块, 它将会拥有轻量级的依赖注入以及监听函数扫描的能力。

Spring Boot?

本节所述中绝大多数内容通用于普通的boot模块和Spring Boot Starter模块。 但是boot模块的实际意义与命名等内容在我们团队中尚存在争议,未来可能会有所调整。

首先,Boot监听需要使用 Boot Application

suspend fun main() {
simbotApplication(Boot) {
beans {
scan("love.forte.example.listeners")
}
}.launch().join()
}

@Beans
class MyListenerContainer {
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}
}
可能更简单

在 Spring Boot Starter 中你可能不需要使用 Boot Application, 而是只是仅仅标记一个 @EnableSimbot 注解就好了:

@EnableSimbot
@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
runApplication<MyApplication>(args = args)
}

@Component
class MyListenerContainer {
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}
}

我们不会在本章节讨论 Spring Boot 环境下的应用。

正如你所见,在 Boot Application 下,你可以通过 beans { } 作用域中的 scan(...) 来进行 包扫描

最终 Boot Application 会扫描所有包路径下标记了 @Beans 的类型,并将它们作为依赖统一管理, 然后解析所有标记了 @Listener 的方法,并尝试将它们解析为监听函数,然后注册。

监听函数

刚刚我们提到,通过标记 @Listener 将一个方法标记为需要解析的监听函数,那么对于这样的函数,它肯定会有一些更多的要求。

可见性

被标记为监听函数的方法的访问级别必须是公开的,也就是必须是 public 的。

@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}
提示

宽松模式下,Kotlin默认的访问级别就是public。

监听类型

一个被标记的监听函数需要有0~1个需要监听的目标事件类型,比如好友事件、群消息事件之类的。 你所需要监听的事件类型直接体现在参数上即可。

监听FooEvent

@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

@Listener
suspend fun FooEvent.onEvent() {
// ...
}

监听所有类型事件

Event 是所有事件类型的父类。

@Listener
suspend fun onEvent(event: Event) {
// ...
}

@Listener
suspend fun Event.onEvent() {
// ...
}

不提供事件类型的参数也将视为监听所有事件。

@Listener
suspend fun onEvent() {
// ...
}
以一为准

不建议一个监听函数的参数中出现多于一个的事件类型参数,如果出现了这种情况, 你应当考虑监听它们共同的某一个父类,或者拆分为多个监听函数来使用。

异步性与返回值

simbot绝大多是API都是可挂起的,因此在Kotlin中,我们也建议你的监听函数是 suspend。 不过比起函数的 suspend 修饰,我们最主要的目的是提醒你尽可能不要使用阻塞API

@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

虽然监听函数的返回值会被包装到 EventResult.of(...) 中或者在没有返回值的情况下得到 EventResult.Invalid, 但是假如函数返回的类型本身就是 EventResult 类型,则不会再被包装,而是直接使用。

因此如果你希望返回一个自定义的 EventResult,直接返回就完事儿了。

事件过滤

消息过滤

在注解监听的世界里,事件的过滤行为也会像 @Listener 那样有所简化:@Filter

@Filter 注解是一个可以提供部分参数来快速过滤消息内容的注解,他需要配合 @Listener 使用,并标记在方法上:

@Filter("你好")
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

上述示例中,即代表事件的 EventListenerProcessingContext.textContext == "你好" 的时候才会触发事件,相当于:

@Listener
suspend fun onEvent(context: EventListenerProcessingContext, event: FooEvent) {
if (context.textContent == "你好") {
// 符合条件,执行逻辑
}
// 不符合条件,不执行逻辑
}

也许你会注意到,textContent可能为null的。默认情况下,只有当监听函数的类型为 MessageEvent(消息事件) 的时候 textContent 才不为null。那么 @Filter("xx") 遇到非消息事件的时候的行为是怎样的呢?

默认情况下,如果 textContent 为null则过滤结果为 false,也就是不符合条件。但是假如你希望当监听到的事件不是消息事件的时候视为通过匹配, 那么可以配置属性 @Filter(value = "xx", ifNullPass = true)

@Filter(value = "你好", ifNullPass = true)
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

除了直接匹配文字之外,@Filter 还提供了其他的匹配策略:@Filter(matcher = ...)

matcher 属性是一个 MatchType 枚举类型,其元素与描述如下:

元素描述
TEXT_EQUALS字符串全等匹配。相当于 text.equals(otherText)
TEXT_EQUALS_IGNORE_CASE字符串全等匹配(忽略大小写)。相当于 text.equalsIgnoreCase(otherText)
TEXT_STARTS_WITH字符串首匹配。相当于 text.startsWith(otherText)
TEXT_ENDS_WITH字符串尾匹配。相当于 text.endsWith(otherText)
TEXT_CONTAINS字符串包含匹配。相当于 text.contains(otherText)
REGEX_MATCHES 默认正则匹配。相当于 regex.matcher(otherText).matches()
REGEX_CONTAINS正则匹配。 相当于 regex.matcher(otherText).find()

由上可见,之前示例中的 Filter("你好") 实际上是通过正则匹配完成的。下面的示例中,我们将改为直接使用字符串全等匹配来实现:

@Filter(value = "你好", matcher = MatchType.TEXT_EQUALS)
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

目标过滤

如果你希望对触发此事件的对象目标 做过滤(例如只能由指定的人或群或bot触发),那么你可以使用 @Filter(targets = @Filter.Targets(...))

@Filter(targets = @Filter.Targets(bots = ["123", "456"]))
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

上述示例代表其标记的监听函数只会被ID为 123456 的BOT触发。 当然,除了 bots,还有一些其他属性可以使用:

属性类型描述
componentsString[]当前事件的所属组件ID
botsString[]当前事件中的BOT ID
authorsString[]当前事件(如果是消息事件的话)的发送者ID
groupsString[]当前事件(如果是群事件的话)的群ID
channelsString[]当前事件(如果是子频道事件的话)的子频道ID
guildsString[]当前事件(如果是频道服务器事件的话)的频道服务器ID
atBotboolean当前事件(如果是消息事件的话)是否存在at当前事件BOT的消息

上述属性中,那些括号中的副条件 如果是xxx事件的话 如果不满足,则其对应的条件匹配将不会生效。 例如一个好友消息,它不属于群消息,因此就算配置了 groups 也等于没配置。

上述属性中,atBot 只会在当前事件类型为 ChatroomMessageEvent 的时候生效。

只是常量

你我都清楚,注解的属性只允许常量值。什么是常量?这不重要,重要的是常量不可修改。 换言之,@Filter.Targets 中的属性都是不可变的。如果你需要更复杂的事件匹配逻辑(例如动态的黑名单), 那么你就不能太过于依赖 @Filter

多条件过滤

如果你想要为一个监听函数提供多个过滤条件,那么多写两次就好了:

@Filter(value = "你", matchType = MatchType.TEXT_STARTS_WITH)
@Filter(value = "好", matchType = MatchType.TEXT_ENDS_WITH)
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

@Filter 是一个可重复注解。默认情况下,当标记了多个 @Filter 时,当其任一生效的时候,事件就会触发。

但是如果你希望多个条件必须全部满足,或者全不满足时该怎么做呢?此时你需要使用 @Filters

@Filters(value = [
Filter(value = "你", matchType = MatchType.TEXT_STARTS_WITH),
Filter(value = "好", matchType = MatchType.TEXT_ENDS_WITH)
],
multiMatchType = MultiFilterMatchType.ALL
)
@Listener
suspend fun onEvent(event: FooEvent) {
// ...
}

@FiltersmultiMatchType 属性为 MultiFilterMatchType 类型的枚举,其元素与描述如下:

元素描述
ANY 默认任意匹配。
ALL全部匹配。
NONE无匹配。

动态参数

也许你会有一个苦恼:我要如何将一个事件中文本消息内容中的一部分提取出来呢?这种场景很常见, 尤其是在一些具有目的性、参数化的监听中。

举个例子,假设你希望用户输入一个 .h{n},而你根据数字 n 来发送一个对应编号的帮助信息。 这种情况下,以目前的情报来说能够实施解决方案大概有如下这些:

我们假设在 GroupMessageEvent 事件中

通过字符串截取并转化:

private val helps = mutableMapOf(
1 to "帮助1",
2 to "帮助2",
10 to "帮助10"
)

@Filter(value = ".h\\d+")
@Listener
suspend fun EventListenerProcessingContext.onEvent(event: GroupMessageEvent) {
// 尝试通过字符串截取获取数字编号
// tips: textContent 在 **消息类型事件** 中中基本不会为null,除非有拦截器对其进行了额外操作。
// 此处保险起见,假若 textContent 为null则使用 event.messageContent.plainText
val numberValue = (textContent ?: event.messageContent.plainText).substring(2)

val number = numberValue.toInt()

event.reply(helps[number] ?: "没有找到编号[$number]的帮助")
}

通过正则提取:

private val helps = mutableMapOf(
1 to "帮助1",
2 to "帮助2",
10 to "帮助10"
)

private const val REGEX_VALUE = ".h(?<number>\\d+)"
private val regex = Regex(REGEX_VALUE)

@Filter(value = REGEX_VALUE)
@Listener
suspend fun EventListenerProcessingContext.onEvent(event: GroupMessageEvent) {
val numberValue = regex.matchEntire(textContent ?: event.messageContent.plainText)?.groups?.get("number")?.value
if (numberValue == null) {
event.reply("没有找到编号")
return
}

val number = numberValue.toInt()

event.reply(helps[number] ?: "没有找到编号[$number]的帮助")
}

或许这种割裂的方式你不喜欢,那么就不再使用 @Filter 了:

private val helps = mutableMapOf(
1 to "帮助1",
2 to "帮助2",
10 to "帮助10"
)

private val regex = Regex(".h(?<number>\\d+)")

@Listener
suspend fun EventListenerProcessingContext.onEvent(event: GroupMessageEvent) {
val text = textContent ?: event.messageContent.plainText
// 自行逻辑匹配,不再借助 @Filter
val matchResult = regex.matchEntire(text) ?: return

val numberValue = matchResult.groups["number"]?.value
if (numberValue == null) {
event.reply("没有找到编号")
return
}

val number = numberValue.toInt()

event.reply(helps[number] ?: "没有找到编号[$number]的帮助")
}

但是总而言之,都会多多少少有些...麻烦。因此,boot模块为开发者提供了一个或许比较有用的注解 @FilterValue。 让我们如下示例:

private val helps = mutableMapOf(
1 to "帮助1",
2 to "帮助2",
10 to "帮助10"
)

@Filter(value = ".h(?<number>\\d+)")
@Listener
suspend fun onEvent(event: GroupMessageEvent, @FilterValue("number") number: Int) {
event.reply(helps[number] ?: "没有找到编号[$number]的帮助")
}

可以看到,当通过正则匹配文本内容时,@FilterValue 可以通过指定一个 group name 来获取此正则匹配的对应结果。 通过这种方式便可以在一定程度上简化样板代码。

当然,除了 (?<NAME>REGEX) 这种形式以外,还有一个较为简化的写法:

private val helps = mutableMapOf(
1 to "帮助1",
2 to "帮助2",
10 to "帮助10"
)

@Filter(value = ".h{{number,\\d+}}")
@Listener
suspend fun onEvent(event: GroupMessageEvent, @FilterValue("number") number: Int) {
event.reply(helps[number] ?: "没有找到编号[$number]的帮助")
}

通过使用 {{}} 进行包裹,并指定名称与其对应的表达式,也可以达到与正则相同结果。

  • {{hello,\\d+}}(?<hello>\\d+) 的效果是一样的。
  • {{hello}}(?<hello>.+) 的效果是一样的。

使用正则原生的能力还是通过 {{...}} 进行一层转化,完全就看你的心情了。如果你对正则比较熟悉,那不妨直接使用 (?<NAME>REGEX) 吧。

正则限定

@FilterValue 仅支持匹配默认为 正则 相关的类型,例如 REGEX_MATCHESREGEX_CONTAINS

类型转化

默认情况下 @FilterValue 的结果仅支持最低限度的简单类型转化(字符串转数字、基础数据类型与包装类型的转化等),而不支持诸如序列化等复杂的类型转化。

参数绑定

你可能会好奇,通过注解监听的时候,到底什么参数能被自动注入、什么参数不能呢? 下面罗列了默认情况下能够被自动注入的监听函数参数:

💠 Event 事件对象

当前监听函数要监听的事件类型。

通过 EventProcessingContext.event 获取并进行类型转化。

@Listener
suspend fun onEvent(event: Event) {
// ...
}

💠 EventListenerProcessingContext

监听函数的事件处理上下文(是 EventProcessingContext 的子类)。

@Listener
suspend fun onEvent(context: EventListenerProcessingContext) { // 或 EventProcessingContext
// ...
}

💠 Application

当前事件所属的 Application

通过 EventProcessingContext.getAttribute(ApplicationAttributes.Application) 获取。

@Listener
suspend fun onEvent(application: Application) {
// ...
}

💠 EventListener

当前监听函数自身。

通过 EventListenerProcessingContext.listener 获取。

@Listener
suspend fun onEvent(listener: EventListener) {
// ...
}

💠 GlobalScopeContext

当前事件中的全局作用域。

通过 EventProcessingContext.getAttribute(SimpleScope.Global) 获取。

@Listener
suspend fun onEvent(globalScope: GlobalScopeContext) {
// ...
}

💠 ContinuousSessionContext

当前事件中提供的 ContinuousSessionContext 实例。

通过 EventProcessingContext.getAttribute(SimpleScope.ContinuousSession) 获取。

@Listener
suspend fun onEvent(continuousSessionContext: ContinuousSessionContext) {
// ...
}

💠 SerializersModule

当前事件所属的 Application 中的序列化模块信息。

通过 EventProcessingContext.messagesSerializersModule 获取。

@Listener
suspend fun onEvent(serializersModule: SerializersModule) {
// ...
}

💠 KFunction

当前监听函数(如果是来自 KFunction)的原始的 KFunction 函数对象。

通过 EventProcessingContext.listener.getAttribute(BootListenerAttributes.RawFunction) 获取。

@Listener
suspend fun onEvent(function: KFunction<*>) {
// ...
}

💠 FilterValue(...)

标记了 @FilterValue 的参数。

@Filter("foo(?<value>.+)")
@Listener
suspend fun onEvent(@FilterValue("value") value: Int) {
// ...
}

其他

当其余所有绑定器都无法进行绑定时,会有一个最终的绑定器来尝试通过依赖注入寻找匹配类型并注入。 以 Spring Boot 项目为例:

@Component
class Foo

// ...

@Listener
suspend fun onEvent(foo: Foo) {
// ...
}

而如果直至最终都无法决定最终注入的内容,则会抛出异常。