diff --git a/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt b/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt index 8b4f3912e..315823f0c 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/Extensions.kt @@ -51,6 +51,44 @@ import kotlin.reflect.KClass public suspend inline fun <reified E : Event> EventChannel<*>.nextEvent( priority: EventPriority = EventPriority.NORMAL, noinline filter: suspend (E) -> Boolean = { true } +): E = nextEvent(priority, false, filter) + +/** + * 挂起当前协程, 直到监听到事件 [E] 的广播并通过 [filter], 返回这个事件实例. + * + * 本函数是 [EventChannel.subscribe] 的衍生工具函数, 内部会调用 [EventChannel.subscribe]. + * + * ## 挂起可取消 + * + * 本函数的挂起过程可以被[取消][CancellableContinuation.cancel]. 这意味着若在 [CoroutineScope.launch] 中使用本函数, 则 [launch] 启动的 [Job] 可以通过 [Job.cancel] 取消 (停止), 届时本函数会抛出 [CancellationException]. + * + * ## 异常处理 + * + * [filter] 抛出的异常属于监听方异常, 将会由 [nextEvent] 原样重新抛出. + * + * ## 使用 [Flow] 的替代方法 + * + * 在 Kotlin 可使用 [EventChannel.asFlow] 配合 [Flow.filter] 和 [Flow.first] 实现与 [nextEvent] 相似的功能 (注意, Flow 方法将会使用 [EventPriority.MONITOR] 优先级). + * + * 示例: + * + * ``` + * val event: GroupMessageEvent = GlobalEventChannel.asFlow().filterIsInstance<GroupMessageEvent>().filter { it.sender.id == 123456 }.first() + * // 上下两行代码等价 + * val event: GroupMessageEvent = GlobalEventChannel.filterIsInstance<GroupMessageEvent>().nextEvent(EventPriority.MONITOR) { it.sender.id == 123456 } + * ``` + * + * 由于 [Flow] 拥有更多操作 (如 [Flow.firstOrNull]), 在不需要指定[事件优先级][EventPriority]时使用 [Flow] 拥有更高自由度. + * + * @param intercept 是否拦截, 传入 `true` 时表示拦截此事件不让接下来的监听器处理, 传入 `false` 时表示让接下来的监听器处理 + * @param filter 过滤器. 返回 `true` 时表示得到了需要的实例. 返回 `false` 时表示继续监听 + * + * @since 2.13 + */ +public suspend inline fun <reified E : Event> EventChannel<*>.nextEvent( + priority: EventPriority = EventPriority.NORMAL, + intercept: Boolean = false, + noinline filter: suspend (E) -> Boolean = { true } ): E = coroutineScope { suspendCancellableCoroutine { cont -> var listener: Listener<E>? = null @@ -61,7 +99,7 @@ public suspend inline fun <reified E : Event> EventChannel<*>.nextEvent( } try { - cont.resumeWith(result) + cont.resumeWith(result.apply { onSuccess { if (intercept) intercept() } }) } finally { listener?.complete() // ensure completed on exceptions } diff --git a/mirai-core-api/src/commonMain/kotlin/message/utils.kt b/mirai-core-api/src/commonMain/kotlin/message/utils.kt index 68f1153d7..826981813 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/utils.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/utils.kt @@ -52,6 +52,28 @@ public suspend inline fun <reified P : MessageEvent> P.nextMessage( timeoutMillis: Long = -1, priority: EventPriority = EventPriority.MONITOR, noinline filter: suspend P.(P) -> Boolean = { true } +): MessageChain = nextMessage(timeoutMillis, priority, false, filter) + +/** + * 挂起当前协程, 等待下一条 [MessageEvent.sender] 和 [MessageEvent.subject] 与 [this] 相同且通过 [筛选][filter] 的 [MessageEvent] 并拦截该事件 + * + * 若 [filter] 抛出了一个异常, 本函数会立即抛出这个异常. + * + * @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制 + * @param intercept 是否拦截, 传入 `true` 时表示拦截此事件不让接下来的监听器处理, 传入 `false` 时表示让接下来的监听器处理 + * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值 + * + * @see syncFromEvent 实现原理 + * @see MessageEvent.intercept 拦截事件 + * + * @since 2.13 + */ +@JvmSynthetic +public suspend inline fun <reified P : MessageEvent> P.nextMessage( + timeoutMillis: Long = -1, + priority: EventPriority = EventPriority.HIGH, + intercept: Boolean = false, + noinline filter: suspend P.(P) -> Boolean = { true } ): MessageChain { val mapper: suspend (P) -> P? = createMapper(filter) @@ -61,7 +83,7 @@ public suspend inline fun <reified P : MessageEvent> P.nextMessage( withTimeout(timeoutMillis) { GlobalEventChannel.syncFromEvent(priority, mapper) } - }).message + }).apply { if (intercept) intercept() }.message } /** diff --git a/mirai-core/src/commonTest/kotlin/event/EventTests.kt b/mirai-core/src/commonTest/kotlin/event/EventTests.kt index 20cc30b02..dee550f0d 100644 --- a/mirai-core/src/commonTest/kotlin/event/EventTests.kt +++ b/mirai-core/src/commonTest/kotlin/event/EventTests.kt @@ -303,4 +303,27 @@ internal class EventTests : AbstractEventTest() { PriorityTestEvent().broadcast() } } + + @Test + fun `test next event and intercept`() { + resetEventListeners() + GlobalEventChannel.subscribeOnce<TestEvent> { + GlobalEventChannel.nextEvent<TestEvent>(priority = EventPriority.HIGH, intercept = true) + } + GlobalEventChannel.subscribeAlways<TestEvent>(priority = EventPriority.LOW) { + this.triggered = true + } + val tmp = TestEvent() + val tmp2 = TestEvent() + runBlocking { + launch { + tmp.broadcast() + } + launch { + tmp2.broadcast() + } + } + assertTrue { (tmp.triggered || tmp2.triggered) && (tmp.triggered != tmp2.triggered) } + resetEventListeners() + } } \ No newline at end of file