From 136b0c11d81125d37570e8e8b2151264cd60a756 Mon Sep 17 00:00:00 2001 From: Him188 <Him188@mamoe.net> Date: Tue, 17 Nov 2020 09:34:47 +0800 Subject: [PATCH] Introduce CommandCallInterceptor --- .../src/command/CommandExecuteResult.kt | 16 ++ .../command/resolve/CommandCallInterceptor.kt | 194 ++++++++++++++++++ .../src/extension/PluginComponentStorage.kt | 16 ++ .../CommandCallInterceptorProvider.kt | 20 ++ .../internal/command/CommandManagerImpl.kt | 32 ++- 5 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 backend/mirai-console/src/command/resolve/CommandCallInterceptor.kt create mode 100644 backend/mirai-console/src/extensions/CommandCallInterceptorProvider.kt diff --git a/backend/mirai-console/src/command/CommandExecuteResult.kt b/backend/mirai-console/src/command/CommandExecuteResult.kt index c1961445c..c306116c3 100644 --- a/backend/mirai-console/src/command/CommandExecuteResult.kt +++ b/backend/mirai-console/src/command/CommandExecuteResult.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.descriptor.* import net.mamoe.mirai.console.command.parse.CommandCall import net.mamoe.mirai.console.command.parse.CommandValueArgument +import net.mamoe.mirai.console.command.resolve.InterceptedReason import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall import net.mamoe.mirai.console.util.ConsoleExperimentalApi import kotlin.contracts.contract @@ -97,6 +98,21 @@ public sealed class CommandExecuteResult { public override val resolvedCall: ResolvedCommandCall? get() = null } + /** 没有匹配的指令 */ + public class Intercepted( + /** 解析的 [CommandCall] (如果匹配到) */ + public override val call: CommandCall?, + /** 解析的 [ResolvedCommandCall] (如果匹配到) */ + public override val resolvedCall: ResolvedCommandCall?, + /** 尝试执行的指令 (如果匹配到) */ + public override val command: Command?, + /** 拦截原因 */ + public val reason: InterceptedReason, + ) : Failure() { + /** 指令执行时发生的错误, 总是 `null` */ + public override val exception: Nothing? get() = null + } + /** 权限不足 */ public class PermissionDenied( /** 尝试执行的指令 */ diff --git a/backend/mirai-console/src/command/resolve/CommandCallInterceptor.kt b/backend/mirai-console/src/command/resolve/CommandCallInterceptor.kt new file mode 100644 index 000000000..bf7529bd3 --- /dev/null +++ b/backend/mirai-console/src/command/resolve/CommandCallInterceptor.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("unused", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.command.resolve + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.parse.CommandCall +import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.extensions.CommandCallInterceptorProvider +import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage +import net.mamoe.mirai.console.internal.util.UNREACHABLE_CLAUSE +import net.mamoe.mirai.console.util.safeCast +import net.mamoe.mirai.message.data.Message +import org.jetbrains.annotations.Contract +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + + +/** + * 指令解析和调用拦截器. 用于在指令各解析阶段拦截或转换调用. + */ +@ExperimentalCommandDescriptors +public interface CommandCallInterceptor { + /** + * 在指令[语法解析][CommandCallParser]前调用. + * + * @return `null` 表示未处理 + */ + public fun interceptBeforeCall( + message: Message, + caller: CommandSender, + ): InterceptResult<Message>? = null + + /** + * 在指令[语法解析][CommandCallParser]后调用. + * + * @return `null` 表示未处理 + */ + public fun interceptCall( + call: CommandCall, + ): InterceptResult<CommandCall>? = null + + /** + * 在指令[调用解析][CommandCallResolver]后调用. + * + * @return `null` 表示未处理 + */ + public fun interceptResolvedCall( + call: ResolvedCommandCall, + ): InterceptResult<ResolvedCommandCall>? = null + + public companion object { + /** + * 使用 [CommandCallInterceptor] 依次调用 [interceptBeforeCall]. + * 在第一个拦截时返回拦截原因, 在所有 [CommandCallInterceptor] 都处理完成后返回结果 [Message] + */ + @JvmStatic + public fun Message.intercepted(caller: CommandSender): InterceptResult<Message> { + GlobalComponentStorage.run { + return CommandCallInterceptorProvider.foldExtensions(this@intercepted) { acc, ext -> + val intercepted = ext.instance.interceptBeforeCall(acc, caller) + intercepted?.fold( + onIntercepted = { return intercepted }, + otherwise = { it } + ) ?: acc + }.let { InterceptResult(it) } + } + } + + /** + * 使用 [CommandCallInterceptor] 依次调用 [interceptBeforeCall]. + * 在第一个拦截时返回拦截原因, 在所有 [CommandCallInterceptor] 都处理完成后返回结果 [CommandCall] + */ + @JvmStatic + public fun CommandCall.intercepted(): InterceptResult<CommandCall> { + GlobalComponentStorage.run { + return CommandCallInterceptorProvider.foldExtensions(this@intercepted) { acc, ext -> + val intercepted = ext.instance.interceptCall(acc) + intercepted?.fold( + onIntercepted = { return intercepted }, + otherwise = { it } + ) ?: acc + }.let { InterceptResult(it) } + } + } + + /** + * 使用 [CommandCallInterceptor] 依次调用 [interceptBeforeCall]. + * 在第一个拦截时返回拦截原因, 在所有 [CommandCallInterceptor] 都处理完成后返回结果 [ResolvedCommandCall] + */ + @JvmStatic + public fun ResolvedCommandCall.intercepted(): InterceptResult<ResolvedCommandCall> { + GlobalComponentStorage.run { + return CommandCallInterceptorProvider.foldExtensions(this@intercepted) { acc, ext -> + val intercepted = ext.instance.interceptResolvedCall(acc) + intercepted?.fold( + onIntercepted = { return intercepted }, + otherwise = { it } + ) ?: acc + }.let { InterceptResult(it) } + } + } + } +} + +/** + * [CommandCallInterceptor] 拦截结果 + */ +@ExperimentalCommandDescriptors +public class InterceptResult<T> internal constructor( + private val _value: Any?, + @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?, +) { + /** + * 构造一个 [InterceptResult], 以 [value] 继续处理后续指令执行. + */ + public constructor(value: T) : this(value as Any?, null) + + /** + * 构造一个 [InterceptResult], 以 [原因][reason] 中断指令执行. + */ + public constructor(reason: InterceptedReason) : this(reason as Any?, null) + + @get:Contract(pure = true) + public val value: T? + @Suppress("UNCHECKED_CAST") + get() { + val value = this._value + return if (value is InterceptedReason) null else value as T + } + + @get:Contract(pure = true) + public val reason: InterceptedReason? + get() = this._value.safeCast() +} + +@ExperimentalCommandDescriptors +public inline fun <T, R> InterceptResult<T>.fold( + onIntercepted: (reason: InterceptedReason) -> R, + otherwise: (call: T) -> R, +): R { + contract { + callsInPlace(onIntercepted, InvocationKind.AT_MOST_ONCE) + callsInPlace(otherwise, InvocationKind.AT_MOST_ONCE) + } + value?.let(otherwise) + reason?.let(onIntercepted) + UNREACHABLE_CLAUSE +} + +@ExperimentalCommandDescriptors +public inline fun <T : R, R> InterceptResult<T>.getOrElse(onIntercepted: (reason: InterceptedReason) -> R): R { + contract { callsInPlace(onIntercepted, InvocationKind.AT_MOST_ONCE) } + reason?.let(onIntercepted) + return value!! +} + +/** + * 创建一个 [InterceptedReason] + * + * @see InterceptedReason.create + */ +@ExperimentalCommandDescriptors +@JvmSynthetic +public inline fun InterceptedReason(message: String): InterceptedReason = InterceptedReason.create(message) + +/** + * 拦截原因 + */ +@ExperimentalCommandDescriptors +public interface InterceptedReason { + public val message: String + + public companion object { + /** + * 创建一个 [InterceptedReason] + */ + public fun create(message: String): InterceptedReason = InterceptedReasonData(message) + } +} + +@OptIn(ExperimentalCommandDescriptors::class) +@Serializable +private data class InterceptedReasonData(override val message: String) : InterceptedReason diff --git a/backend/mirai-console/src/extension/PluginComponentStorage.kt b/backend/mirai-console/src/extension/PluginComponentStorage.kt index 4cb05874f..601a19dde 100644 --- a/backend/mirai-console/src/extension/PluginComponentStorage.kt +++ b/backend/mirai-console/src/extension/PluginComponentStorage.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.console.extension import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.command.resolve.CommandCallInterceptor import net.mamoe.mirai.console.command.resolve.CommandCallResolver import net.mamoe.mirai.console.extensions.* import net.mamoe.mirai.console.internal.extension.AbstractConcurrentComponentStorage @@ -128,4 +129,19 @@ public class PluginComponentStorage( @OverloadResolutionByLambdaReturnType public fun contributeCommandCallParser(provider: CommandCallResolverProvider): Unit = contribute(CommandCallResolverProvider, plugin, provider) + + ///////////////////////////////////// + + /** 注册一个 [CommandCallInterceptorProvider] */ + @ExperimentalCommandDescriptors + @OverloadResolutionByLambdaReturnType + public fun contributeCommandCallInterceptor(lazyInstance: () -> CommandCallInterceptor): Unit = + contribute(CommandCallInterceptorProvider, plugin, CommandCallInterceptorProviderImplLazy(lazyInstance)) + + /** 注册一个 [CommandCallInterceptorProvider] */ + @ExperimentalCommandDescriptors + @JvmName("contributeCommandCallInterceptorProvider") + @OverloadResolutionByLambdaReturnType + public fun contributeCommandCallParser(provider: CommandCallInterceptorProvider): Unit = + contribute(CommandCallInterceptorProvider, plugin, provider) } \ No newline at end of file diff --git a/backend/mirai-console/src/extensions/CommandCallInterceptorProvider.kt b/backend/mirai-console/src/extensions/CommandCallInterceptorProvider.kt new file mode 100644 index 000000000..7159ec4c6 --- /dev/null +++ b/backend/mirai-console/src/extensions/CommandCallInterceptorProvider.kt @@ -0,0 +1,20 @@ +package net.mamoe.mirai.console.extensions + +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.resolve.CommandCallInterceptor +import net.mamoe.mirai.console.extension.AbstractInstanceExtensionPoint +import net.mamoe.mirai.console.extension.InstanceExtension + +@ExperimentalCommandDescriptors +public interface CommandCallInterceptorProvider : InstanceExtension<CommandCallInterceptor> { + public companion object ExtensionPoint : + AbstractInstanceExtensionPoint<CommandCallInterceptorProvider, CommandCallInterceptor>(CommandCallInterceptorProvider::class) +} + +@ExperimentalCommandDescriptors +public class CommandCallInterceptorProviderImpl(override val instance: CommandCallInterceptor) : CommandCallInterceptorProvider + +@ExperimentalCommandDescriptors +public class CommandCallInterceptorProviderImplLazy(initializer: () -> CommandCallInterceptor) : CommandCallInterceptorProvider { + override val instance: CommandCallInterceptor by lazy(initializer) +} \ No newline at end of file diff --git a/backend/mirai-console/src/internal/command/CommandManagerImpl.kt b/backend/mirai-console/src/internal/command/CommandManagerImpl.kt index 4818c1c1e..a198b7ed6 100644 --- a/backend/mirai-console/src/internal/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/internal/command/CommandManagerImpl.kt @@ -20,8 +20,11 @@ import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall +import net.mamoe.mirai.console.command.resolve.CommandCallInterceptor.Companion.intercepted import net.mamoe.mirai.console.command.resolve.CommandCallResolver.Companion.resolve +import net.mamoe.mirai.console.command.resolve.getOrElse import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.internal.util.ifNull import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.event.Listener @@ -173,15 +176,32 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiCons // Don't move into CommandManager, compilation error / VerifyError @OptIn(ExperimentalCommandDescriptors::class) internal suspend fun executeCommandImpl( - message: Message, + message0: Message, caller: CommandSender, checkPermission: Boolean, ): CommandExecuteResult { - val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCommand(null) - val resolved = call.resolve().fold( - onSuccess = { it }, - onFailure = { return it } - ) ?: return CommandExecuteResult.UnresolvedCommand(call) + val message = message0 + .intercepted(caller) + .getOrElse { return CommandExecuteResult.Intercepted(null, null, null, it) } + + val call = message.asMessageChain() + .parseCommandCall(caller) + .ifNull { return CommandExecuteResult.UnresolvedCommand(null) } + .let { raw -> + raw.intercepted() + .getOrElse { return CommandExecuteResult.Intercepted(raw, null, null, it) } + } + + val resolved = call + .resolve().fold( + onSuccess = { it }, + onFailure = { return it } + ) + .ifNull { return CommandExecuteResult.UnresolvedCommand(call) } + .let { raw -> + raw.intercepted() + .getOrElse { return CommandExecuteResult.Intercepted(call, raw, null, it) } + } val command = resolved.callee