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