From eeb361358ab5d97ca08e7143047389ed2fad595f Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 15 Jun 2022 12:39:01 +0100 Subject: [PATCH] Add CommandContext and support retrieving original message chain from command, close #1835 --- .../compatibility-validation/jvm/api/jvm.api | 42 ++- .../src/command/CommandContext.kt | 37 +++ .../src/command/CompositeCommand.kt | 14 +- .../mirai-console/src/command/RawCommand.kt | 30 +- .../command/descriptor/CommandParameter.kt | 56 +++- .../command/descriptor/CommandSignature.kt | 8 +- .../src/command/java/JRawCommand.kt | 24 +- .../src/command/parse/CommandCall.kt | 9 +- .../parse/SpaceSeparatedCommandCallParser.kt | 3 +- .../resolve/BuiltInCommandCallResolver.kt | 29 +- .../command/resolve/ResolvedCommandCall.kt | 9 +- .../src/internal/command/CommandReflector.kt | 61 ++-- .../test/command/AbstractCommandTest.kt | 4 + .../test/command/CommandContextTest.kt | 263 ++++++++++++++++++ .../test/command/InstanceTestCommand.kt | 2 +- 15 files changed, 508 insertions(+), 83 deletions(-) create mode 100644 mirai-console/backend/mirai-console/src/command/CommandContext.kt create mode 100644 mirai-console/backend/mirai-console/test/command/CommandContextTest.kt diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index 9a5b823e1..855a8f1b5 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -171,6 +171,11 @@ public final class net/mamoe/mirai/console/command/Command$Companion { public final fun getAllNames (Lnet/mamoe/mirai/console/command/Command;)[Ljava/lang/String; } +public abstract interface class net/mamoe/mirai/console/command/CommandContext { + public abstract fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain; + public abstract fun getSender ()Lnet/mamoe/mirai/console/command/CommandSender; +} + public abstract class net/mamoe/mirai/console/command/CommandExecuteResult { public abstract fun getCall ()Lnet/mamoe/mirai/console/command/parse/CommandCall; public abstract fun getCommand ()Lnet/mamoe/mirai/console/command/Command; @@ -571,7 +576,8 @@ public abstract class net/mamoe/mirai/console/command/RawCommand : net/mamoe/mir public fun getPrimaryName ()Ljava/lang/String; public fun getSecondaryNames ()[Ljava/lang/String; public fun getUsage ()Ljava/lang/String; - public abstract fun onCommand (Lnet/mamoe/mirai/console/command/CommandSender;Lnet/mamoe/mirai/message/data/MessageChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun onCommand (Lnet/mamoe/mirai/console/command/CommandContext;Lnet/mamoe/mirai/message/data/MessageChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun onCommand (Lnet/mamoe/mirai/console/command/CommandSender;Lnet/mamoe/mirai/message/data/MessageChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract class net/mamoe/mirai/console/command/SimpleCommand : net/mamoe/mirai/console/command/AbstractCommand, net/mamoe/mirai/console/command/Command, net/mamoe/mirai/console/command/descriptor/CommandArgumentContextAware { @@ -855,25 +861,28 @@ public abstract interface class net/mamoe/mirai/console/command/descriptor/Comma public abstract fun isOptional ()Z } -public final class net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter : net/mamoe/mirai/console/command/descriptor/AbstractCommandParameter, net/mamoe/mirai/console/command/descriptor/CommandParameter { +public abstract class net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter : net/mamoe/mirai/console/command/descriptor/AbstractCommandParameter, net/mamoe/mirai/console/command/descriptor/CommandParameter { public static final field Companion Lnet/mamoe/mirai/console/command/descriptor/CommandReceiverParameter$Companion; public static final field NAME Ljava/lang/String; - public fun (ZLkotlin/reflect/KType;)V - public final fun component1 ()Z - public final fun component2 ()Lkotlin/reflect/KType; - public final fun copy (ZLkotlin/reflect/KType;)Lnet/mamoe/mirai/console/command/descriptor/CommandReceiverParameter; - public static synthetic fun copy$default (Lnet/mamoe/mirai/console/command/descriptor/CommandReceiverParameter;ZLkotlin/reflect/KType;ILjava/lang/Object;)Lnet/mamoe/mirai/console/command/descriptor/CommandReceiverParameter; - public fun equals (Ljava/lang/Object;)Z public fun getName ()Ljava/lang/String; - public fun getType ()Lkotlin/reflect/KType; - public fun hashCode ()I - public fun isOptional ()Z - public fun toString ()Ljava/lang/String; } public final class net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter$Companion { } +public final class net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter$Context : net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter { + public fun (ZLkotlin/reflect/KType;)V + public synthetic fun (ZLkotlin/reflect/KType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getType ()Lkotlin/reflect/KType; + public fun isOptional ()Z +} + +public final class net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter$Sender : net/mamoe/mirai/console/command/descriptor/CommandReceiverParameter { + public fun (ZLkotlin/reflect/KType;)V + public fun getType ()Lkotlin/reflect/KType; + public fun isOptional ()Z +} + public class net/mamoe/mirai/console/command/descriptor/CommandResolutionException : java/lang/RuntimeException { public fun ()V public fun (Ljava/lang/String;)V @@ -1118,6 +1127,7 @@ public abstract class net/mamoe/mirai/console/command/java/JRawCommand : net/mam public fun getPrimaryName ()Ljava/lang/String; public fun getSecondaryNames ()[Ljava/lang/String; public fun getUsage ()Ljava/lang/String; + public fun onCommand (Lnet/mamoe/mirai/console/command/CommandContext;Lnet/mamoe/mirai/message/data/MessageChain;)V public fun onCommand (Lnet/mamoe/mirai/console/command/CommandSender;Lnet/mamoe/mirai/message/data/MessageChain;)V protected final fun setDescription (Ljava/lang/String;)V protected final fun setPermission (Lnet/mamoe/mirai/console/permission/Permission;)V @@ -1145,13 +1155,15 @@ public abstract interface class net/mamoe/mirai/console/command/parse/CommandArg public abstract interface class net/mamoe/mirai/console/command/parse/CommandCall { public abstract fun getCalleeName ()Ljava/lang/String; public abstract fun getCaller ()Lnet/mamoe/mirai/console/command/CommandSender; + public abstract fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain; public abstract fun getValueArguments ()Ljava/util/List; } public final class net/mamoe/mirai/console/command/parse/CommandCallImpl : net/mamoe/mirai/console/command/parse/CommandCall { - public fun (Lnet/mamoe/mirai/console/command/CommandSender;Ljava/lang/String;Ljava/util/List;)V + public fun (Lnet/mamoe/mirai/console/command/CommandSender;Ljava/lang/String;Ljava/util/List;Lnet/mamoe/mirai/message/data/MessageChain;)V public fun getCalleeName ()Ljava/lang/String; public fun getCaller ()Lnet/mamoe/mirai/console/command/CommandSender; + public fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain; public fun getValueArguments ()Ljava/util/List; } @@ -1234,6 +1246,7 @@ public abstract interface class net/mamoe/mirai/console/command/resolve/Resolved public abstract fun getCallee ()Lnet/mamoe/mirai/console/command/Command; public abstract fun getCalleeSignature ()Lnet/mamoe/mirai/console/command/descriptor/CommandSignature; public abstract fun getCaller ()Lnet/mamoe/mirai/console/command/CommandSender; + public abstract fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain; public abstract fun getRawValueArguments ()Ljava/util/List; public abstract fun getResolvedValueArguments ()Ljava/util/List; } @@ -1242,10 +1255,11 @@ public final class net/mamoe/mirai/console/command/resolve/ResolvedCommandCall$C } public final class net/mamoe/mirai/console/command/resolve/ResolvedCommandCallImpl : net/mamoe/mirai/console/command/resolve/ResolvedCommandCall { - public fun (Lnet/mamoe/mirai/console/command/CommandSender;Lnet/mamoe/mirai/console/command/Command;Lnet/mamoe/mirai/console/command/descriptor/CommandSignature;Ljava/util/List;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;)V + public fun (Lnet/mamoe/mirai/console/command/CommandSender;Lnet/mamoe/mirai/console/command/Command;Lnet/mamoe/mirai/console/command/descriptor/CommandSignature;Ljava/util/List;Lnet/mamoe/mirai/console/command/descriptor/CommandArgumentContext;Lnet/mamoe/mirai/message/data/MessageChain;)V public fun getCallee ()Lnet/mamoe/mirai/console/command/Command; public fun getCalleeSignature ()Lnet/mamoe/mirai/console/command/descriptor/CommandSignature; public fun getCaller ()Lnet/mamoe/mirai/console/command/CommandSender; + public fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain; public fun getRawValueArguments ()Ljava/util/List; public fun getResolvedValueArguments ()Ljava/util/List; } diff --git a/mirai-console/backend/mirai-console/src/command/CommandContext.kt b/mirai-console/backend/mirai-console/src/command/CommandContext.kt new file mode 100644 index 000000000..4a8073901 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/command/CommandContext.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.utils.NotStableForInheritance + +/** + * 指令执行环境 + * @since 2.12 + */ +@NotStableForInheritance +public interface CommandContext { + /** + * 指令发送者 + */ + public val sender: CommandSender + + /** + * 触发指令的原消息链,包含元数据,也包含指令名。 + * + * 示例内容:`messageChainOf(MessageSource(...), PlainText("/test"), PlainText("arg1"))` + */ + public val originalMessage: MessageChain +} + +internal class CommandContextImpl( + override val sender: CommandSender, + override val originalMessage: MessageChain +) : CommandContext \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt index 525acb4f7..f14ac0bae 100644 --- a/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/CompositeCommand.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. + * 此源代码的使用受 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 + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.console.command @@ -66,6 +66,12 @@ import kotlin.annotation.AnnotationTarget.FUNCTION * sendMessage("/manage list 被调用了") * } * + * @SubCommand + * suspend fun CommandContext.repeat() { + * // 使用 CommandContext 作为参数, + * sendMessage("/manage list 被调用了") + * } + * * // 支持 Image 类型, 需在聊天中执行此指令. * @SubCommand * suspend fun UserCommandSender.test(image: Image) { // 执行 "/manage test <一张图片>" 时调用这个函数 diff --git a/mirai-console/backend/mirai-console/src/command/RawCommand.kt b/mirai-console/backend/mirai-console/src/command/RawCommand.kt index f9d30fbe3..50564f06f 100644 --- a/mirai-console/backend/mirai-console/src/command/RawCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/RawCommand.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. + * 此源代码的使用受 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 + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.console.command @@ -19,7 +19,6 @@ import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.buildMessageChain -import kotlin.reflect.typeOf /** * 无参数解析, 接收原生参数的指令. @@ -60,7 +59,7 @@ public abstract class RawCommand( @ExperimentalCommandDescriptors override val overloads: List<@JvmWildcard CommandSignature> = listOf( CommandSignatureImpl( - receiverParameter = CommandReceiverParameter(false, typeOf()), + receiverParameter = CommandReceiverParameter.Context(false), valueParameters = listOf( AbstractCommandValueParameter.UserDefinedType.createRequired>( "args", @@ -70,7 +69,8 @@ public abstract class RawCommand( ) { call -> val sender = call.caller val arguments = call.rawValueArguments - sender.onCommand(buildMessageChain { arguments.forEach { +it.value } }) + val context = CommandContextImpl(sender, call.originalMessage) + context.onCommand(buildMessageChain { arguments.forEach { +it.value } }) } ) @@ -81,7 +81,21 @@ public abstract class RawCommand( * * @see CommandManager.executeCommand 查看更多信息 */ - public abstract suspend fun CommandSender.onCommand(args: MessageChain) + public open suspend fun CommandSender.onCommand(args: MessageChain) { + + } + + /** + * 在指令被执行时调用. + * + * @param args 指令参数. + * @see CommandManager.executeCommand 查看更多信息 + * + * @since 2.12 + */ + public open suspend fun CommandContext.onCommand(args: MessageChain) { + return sender.onCommand(args) + } } diff --git a/mirai-console/backend/mirai-console/src/command/descriptor/CommandParameter.kt b/mirai-console/backend/mirai-console/src/command/descriptor/CommandParameter.kt index 4a10d0e71..df3bbf6ee 100644 --- a/mirai-console/backend/mirai-console/src/command/descriptor/CommandParameter.kt +++ b/mirai-console/backend/mirai-console/src/command/descriptor/CommandParameter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -9,6 +9,7 @@ package net.mamoe.mirai.console.command.descriptor +import net.mamoe.mirai.console.command.CommandContext import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createOptional import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createRequired @@ -122,22 +123,50 @@ public sealed class ArgumentAcceptance( } @ExperimentalCommandDescriptors -public data class CommandReceiverParameter( - override val isOptional: Boolean, - override val type: KType, +public sealed class CommandReceiverParameter( ) : CommandParameter, AbstractCommandParameter() { - override val name: String get() = NAME - init { - val classifier = type.classifier - require(classifier is KClass<*>) { - "CommandReceiverParameter.type.classifier must be KClass." - } - require(classifier.isSubclassOf(CommandSender::class)) { - "CommandReceiverParameter.type.classifier must be subclass of CommandSender." + /** + * @since 2.12 + */ + @ExperimentalCommandDescriptors + public class Sender( + override val isOptional: Boolean, + override val type: KType, + ) : CommandReceiverParameter() { + init { + val classifier = type.classifier + require(classifier is KClass<*>) { + "CommandReceiverParameter.Sender.type.classifier must be KClass." + } + require(classifier.isSubclassOf(CommandSender::class)) { + "CommandReceiverParameter.Sender.type.classifier must be subclass of CommandSender." + } } } + /** + * @since 2.12 + */ + @ExperimentalCommandDescriptors + public class Context( + override val isOptional: Boolean, + override val type: KType = typeOf(), + ) : CommandReceiverParameter() { + init { + val classifier = type.classifier + require(classifier is KClass<*>) { + "CommandReceiverParameter.Context.type.classifier must be KClass." + } + require(classifier.isSubclassOf(CommandContext::class)) { + "CommandReceiverParameter.Context.type.classifier must be subclass of CommandContext." + } + } + } + + override val name: String get() = NAME + + public companion object { public const val NAME: String = "" } @@ -221,7 +250,6 @@ public sealed class AbstractCommandValueParameter : CommandValueParameter, } private companion object { - @OptIn(ExperimentalStdlibApi::class) val STRING_TYPE = typeOf() } } @@ -251,13 +279,11 @@ public sealed class AbstractCommandValueParameter : CommandValueParameter, public companion object { @JvmStatic public inline fun createOptional(name: String, isVararg: Boolean): UserDefinedType { - @OptIn(ExperimentalStdlibApi::class) return UserDefinedType(name, true, isVararg, typeOf()) } @JvmStatic public inline fun createRequired(name: String, isVararg: Boolean): UserDefinedType { - @OptIn(ExperimentalStdlibApi::class) return UserDefinedType(name, false, isVararg, typeOf()) } } diff --git a/mirai-console/backend/mirai-console/src/command/descriptor/CommandSignature.kt b/mirai-console/backend/mirai-console/src/command/descriptor/CommandSignature.kt index cbb8380a9..0a08ec62d 100644 --- a/mirai-console/backend/mirai-console/src/command/descriptor/CommandSignature.kt +++ b/mirai-console/backend/mirai-console/src/command/descriptor/CommandSignature.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -25,7 +25,7 @@ public interface CommandSignature { * 接收者参数, 为 [CommandSender] 子类 */ @ConsoleExperimentalApi - public val receiverParameter: CommandReceiverParameter? + public val receiverParameter: CommandReceiverParameter<*>? /** * 形式 值参数. @@ -67,7 +67,7 @@ public abstract class AbstractCommandSignature : CommandSignature { @ExperimentalCommandDescriptors public open class CommandSignatureImpl( - override val receiverParameter: CommandReceiverParameter?, + override val receiverParameter: CommandReceiverParameter<*>?, override val valueParameters: List>, private val onCall: suspend CommandSignatureImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, ) : CommandSignature, AbstractCommandSignature() { @@ -79,7 +79,7 @@ public open class CommandSignatureImpl( @ConsoleExperimentalApi @ExperimentalCommandDescriptors public open class CommandSignatureFromKFunctionImpl( - override val receiverParameter: CommandReceiverParameter?, + override val receiverParameter: CommandReceiverParameter<*>?, override val valueParameters: List>, override val originFunction: KFunction<*>, private val onCall: suspend CommandSignatureFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit, diff --git a/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt b/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt index 01f1555b4..63f4bd78e 100644 --- a/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -20,7 +20,6 @@ import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.buildMessageChain import net.mamoe.mirai.utils.runBIO -import kotlin.reflect.typeOf /** * 供 Java 用户继承 @@ -82,7 +81,7 @@ public abstract class JRawCommand @ExperimentalCommandDescriptors override val overloads: List<@JvmWildcard CommandSignature> = listOf( CommandSignatureImpl( - receiverParameter = CommandReceiverParameter(false, typeOf()), + receiverParameter = CommandReceiverParameter.Context(false), valueParameters = listOf( AbstractCommandValueParameter.UserDefinedType.createRequired>( "args", @@ -92,7 +91,12 @@ public abstract class JRawCommand ) { call -> val sender = call.caller val arguments = call.rawValueArguments - runBIO { onCommand(sender, buildMessageChain { arguments.forEach { +it.value } }) } + runBIO { + onCommand( + CommandContextImpl(sender, call.originalMessage), + buildMessageChain { arguments.forEach { +it.value } } + ) + } } ) @@ -105,4 +109,16 @@ public abstract class JRawCommand * @since 2.8 */ public open fun onCommand(sender: CommandSender, args: MessageChain) {} + + /** + * 在指令被执行时调用. + * + * @param args 指令参数. + * + * @see CommandManager.executeCommand 查看更多信息 + * @since 2.12 + */ + public open fun onCommand(context: CommandContext, args: MessageChain) { + onCommand(context.sender, args) + } } diff --git a/mirai-console/backend/mirai-console/src/command/parse/CommandCall.kt b/mirai-console/backend/mirai-console/src/command/parse/CommandCall.kt index c86d38c50..57ea066f2 100644 --- a/mirai-console/backend/mirai-console/src/command/parse/CommandCall.kt +++ b/mirai-console/backend/mirai-console/src/command/parse/CommandCall.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -50,6 +50,12 @@ public interface CommandCall { */ public val valueArguments: List + /** + * Original message + * @since 2.12 + */ + public val originalMessage: MessageChain + // maybe add contextual arguments, i.e. from MessageMetadata } @@ -58,4 +64,5 @@ public class CommandCallImpl( override val caller: CommandSender, override val calleeName: String, override val valueArguments: List, + override val originalMessage: MessageChain, ) : CommandCall \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/parse/SpaceSeparatedCommandCallParser.kt b/mirai-console/backend/mirai-console/src/command/parse/SpaceSeparatedCommandCallParser.kt index 785d9444f..299fd8cd1 100644 --- a/mirai-console/backend/mirai-console/src/command/parse/SpaceSeparatedCommandCallParser.kt +++ b/mirai-console/backend/mirai-console/src/command/parse/SpaceSeparatedCommandCallParser.kt @@ -35,7 +35,8 @@ public object SpaceSeparatedCommandCallParser : CommandCallParser { return CommandCallImpl( caller = caller, calleeName = flatten.first().content, - valueArguments = flatten.drop(1).map(::DefaultCommandValueArgument) + valueArguments = flatten.drop(1).map(::DefaultCommandValueArgument), + originalMessage = message ) } } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/resolve/BuiltInCommandCallResolver.kt b/mirai-console/backend/mirai-console/src/command/resolve/BuiltInCommandCallResolver.kt index b4849ef6e..fa0c2a4fc 100644 --- a/mirai-console/backend/mirai-console/src/command/resolve/BuiltInCommandCallResolver.kt +++ b/mirai-console/backend/mirai-console/src/command/resolve/BuiltInCommandCallResolver.kt @@ -52,7 +52,8 @@ public object BuiltInCommandCallResolver : CommandCallResolver { callee, signature.signature, signature.zippedArguments.map { it.second }, - context ?: EmptyCommandArgumentContext + context ?: EmptyCommandArgumentContext, + call.originalMessage, ) ) } @@ -101,14 +102,24 @@ public object BuiltInCommandCallResolver : CommandCallResolver { ): ResolveData? { val signature = this val receiverParameter = signature.receiverParameter - if (receiverParameter?.type?.classifierAsKClass()?.isInstance(caller) == false) { - errorSink.reportUnmatched( - UnmatchedCommandSignature( - signature, - FailureReason.InapplicableReceiverArgument(receiverParameter, caller) - ) - )// not compatible receiver - return null + + if (receiverParameter != null) { + when (receiverParameter) { + is CommandReceiverParameter.Context -> { + // accepts any sender + } + is CommandReceiverParameter.Sender -> { + if (!receiverParameter.type.classifierAsKClass().isInstance(caller)) { + errorSink.reportUnmatched( + UnmatchedCommandSignature( + signature, + FailureReason.InapplicableReceiverArgument(receiverParameter, caller) + ) + )// not compatible receiver + return null + } + } + } } val valueParameters = signature.valueParameters diff --git a/mirai-console/backend/mirai-console/src/command/resolve/ResolvedCommandCall.kt b/mirai-console/backend/mirai-console/src/command/resolve/ResolvedCommandCall.kt index 9cdea5fd0..401330c21 100644 --- a/mirai-console/backend/mirai-console/src/command/resolve/ResolvedCommandCall.kt +++ b/mirai-console/backend/mirai-console/src/command/resolve/ResolvedCommandCall.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -20,6 +20,7 @@ import net.mamoe.mirai.console.command.parse.mapToTypeOrNull import net.mamoe.mirai.console.internal.data.classifierAsKClass import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.cast +import net.mamoe.mirai.message.data.MessageChain /** * The resolved [CommandCall]. @@ -60,6 +61,11 @@ public interface ResolvedCommandCall { @ConsoleExperimentalApi public val resolvedValueArguments: List> + /** + * @since 2.12 + */ + public val originalMessage: MessageChain + public companion object } @@ -94,6 +100,7 @@ public class ResolvedCommandCallImpl( override val calleeSignature: CommandSignature, override val rawValueArguments: List, private val context: CommandArgumentContext, + override val originalMessage: MessageChain, ) : ResolvedCommandCall { override val resolvedValueArguments: List> by lazy { calleeSignature.valueParameters.zip(rawValueArguments).map { (parameter, argument) -> diff --git a/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt b/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt index a4d3cd809..ba01d7ccf 100644 --- a/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt +++ b/mirai-console/backend/mirai-console/src/internal/command/CommandReflector.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -19,10 +19,7 @@ import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.SingleMessage import net.mamoe.mirai.message.data.buildMessageChain import net.mamoe.mirai.utils.runBIO -import kotlin.reflect.KFunction -import kotlin.reflect.KParameter -import kotlin.reflect.KType -import kotlin.reflect.KVisibility +import kotlin.reflect.* import kotlin.reflect.full.* @@ -130,7 +127,7 @@ public class IllegalCommandDeclarationException : Exception { @OptIn(ExperimentalCommandDescriptors::class) internal class CommandReflector( val command: Command, - val annotationResolver: SubCommandAnnotationResolver, + private val annotationResolver: SubCommandAnnotationResolver, ) { @Suppress("NOTHING_TO_INLINE") @@ -143,8 +140,11 @@ internal class CommandReflector( private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.hasAnnotation(command, this) private fun KFunction<*>.checkExtensionReceiver() { this.extensionReceiverParameter?.let { receiver -> - if (receiver.type.classifierAsKClassOrNull()?.isSubclassOf(CommandSender::class) != true) { - illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender.") + val classifier = receiver.type.classifierAsKClassOrNull() + if (classifier != null) { + if (!classifier.isSubclassOf(CommandSender::class) && !classifier.isSubclassOf(CommandContext::class)) { + illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender nor CommandContext.") + } } } } @@ -279,8 +279,8 @@ internal class CommandReflector( var receiverParameter = function.extensionReceiverParameter if (receiverParameter == null && valueParameters.isNotEmpty()) { val valueFirstParameter = valueParameters[0] - if (valueFirstParameter.type.classifierAsKClassOrNull() - ?.isSubclassOf(CommandSender::class) == true + val classifier = valueFirstParameter.type.classifierAsKClassOrNull() + if (classifier != null && isAcceptableReceiverType(classifier) ) { receiverParameter = valueFirstParameter valueParameters.removeAt(0) @@ -317,14 +317,22 @@ internal class CommandReflector( } if (receiverParameter != null) { - check(receiverParameter.type.classifierAsKClass().isInstance(call.caller)) { - "Bad command call resolved. " + - "Function expects receiver parameter ${receiverParameter.type} whereas actual is ${call.caller::class}." + + val receiverType = receiverParameter.type.classifierAsKClass() + + if (receiverType.isSubclassOf(CommandContext::class)) { + args[receiverParameter] = CommandContextImpl(call.caller, call.originalMessage) + } else { + check(receiverType.isInstance(call.caller)) { + "Bad command call resolved. " + + "Function expects receiver parameter ${receiverParameter.type} whereas actual is ${call.caller::class}." + } + args[receiverParameter] = call.caller } - args[receiverParameter] = call.caller + } - // #341 + // mirai-console#341 if (function.isSuspend) { function.callSuspendBy(args) } else { @@ -334,18 +342,29 @@ internal class CommandReflector( }.toList() } + private fun isAcceptableReceiverType(classifier: KClass) = + classifier.isSubclassOf(CommandSender::class) || classifier.isSubclassOf(CommandContext::class) + + @Suppress("SameParameterValue") private fun createMapEntry(key: K, value: V) = object : Map.Entry { override val key: K get() = key override val value: V get() = value } - private fun KParameter.toCommandReceiverParameter(): CommandReceiverParameter { + private fun KParameter.toCommandReceiverParameter(): CommandReceiverParameter<*> { check(!this.isVararg) { "Receiver cannot be vararg." } - check( - this.type.classifierAsKClass().isSubclassOf(CommandSender::class) - ) { "Receiver must be subclass of CommandSender" } - - return CommandReceiverParameter(this.type.isMarkedNullable, this.type) + val classifier = type.classifierAsKClass() + return when { + classifier.isSubclassOf(CommandSender::class) -> { + CommandReceiverParameter.Sender(this.type.isMarkedNullable, this.type) + } + classifier.isSubclassOf(CommandContext::class) -> { + CommandReceiverParameter.Context(this.type.isMarkedNullable, this.type) + } + else -> { + throw IllegalArgumentException("Receiver must be subclass of CommandSender or CommandContext") + } + } } private fun createStringConstantParameterForName( diff --git a/mirai-console/backend/mirai-console/test/command/AbstractCommandTest.kt b/mirai-console/backend/mirai-console/test/command/AbstractCommandTest.kt index b53d6410f..3dab1695d 100644 --- a/mirai-console/backend/mirai-console/test/command/AbstractCommandTest.kt +++ b/mirai-console/backend/mirai-console/test/command/AbstractCommandTest.kt @@ -16,4 +16,8 @@ import net.mamoe.mirai.console.testFramework.AbstractConsoleInstanceTest internal abstract class AbstractCommandTest : AbstractConsoleInstanceTest() { val dataScope get() = DataScope as ConsoleDataScopeImpl val consoleSender get() = ConsoleCommandSender + + + open val sender: CommandSender get() = ConsoleCommandSender + open val owner: CommandOwner get() = ConsoleCommandOwner } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt b/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt new file mode 100644 index 000000000..1a59e80b6 --- /dev/null +++ b/mirai-console/backend/mirai-console/test/command/CommandContextTest.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2019-2022 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/dev/LICENSE + */ + +@file:OptIn(ExperimentalCommandDescriptors::class) +@file:Suppress("unused", "UNUSED_PARAMETER") + +package net.mamoe.mirai.console.command + +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.console.Testing +import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors +import net.mamoe.mirai.console.command.java.JCompositeCommand +import net.mamoe.mirai.console.command.java.JRawCommand +import net.mamoe.mirai.console.command.java.JSimpleCommand +import net.mamoe.mirai.console.internal.data.classifierAsKClass +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.safeCast +import org.apache.commons.lang3.ArrayUtils.isSameType +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import kotlin.test.assertEquals + +internal class CommandContextTest : AbstractCommandTest() { + private class MyMetadata : MessageMetadata { + override fun hashCode(): Int = javaClass.hashCode() + override fun equals(other: Any?): Boolean = isSameType(this, other) + + override fun toString(): String = "MyMetadata" + + companion object Key : AbstractMessageKey({ it.safeCast() }) + } + + + /////////////////////////////////////////////////////////////////////////// + // RawCommand + /////////////////////////////////////////////////////////////////////////// + + @TestFactory + fun `can execute with sender`(): List { + return listOf( + object : RawCommand(owner, "test") { + override suspend fun CommandContext.onCommand(args: MessageChain) { + Testing.ok(args) + } + } to "/test foo", + object : SimpleCommand(owner, "test") { + @Handler + fun CommandContext.foo(arg: MessageChain) { + Testing.ok(arg) + } + } to "/test foo", + object : CompositeCommand(owner, "test") { + @SubCommand + fun CommandContext.sub(arg: MessageChain) { + Testing.ok(arg) + } + } to "/test sub foo", + + object : JRawCommand(owner, "test") { + override fun onCommand(context: CommandContext, args: MessageChain) { + Testing.ok(args) + } + } to "/test foo", + object : JSimpleCommand(owner, "test") { + @Handler + fun foo(context: CommandContext, arg: MessageChain) { + Testing.ok(arg) + } + } to "/test foo", + object : JCompositeCommand(owner, "test") { + @SubCommand + fun sub(context: CommandContext, arg: MessageChain) { + Testing.ok(arg) + } + } to "/test sub foo", + ).map { (instance, cmd) -> + DynamicTest.dynamicTest(instance::class.supertypes.first().classifierAsKClass().simpleName) { + runBlocking { + instance.withRegistration { + assertEquals( + messageChainOf(PlainText("foo")), + Testing.withTesting { + assertSuccess(sender.executeCommand(cmd, checkPermission = false)) + } + ) + } + } + } + } + } + + @TestFactory + fun `RawCommand can execute and get original chain`(): List { + return listOf( + object : RawCommand(owner, "test") { + override suspend fun CommandContext.onCommand(args: MessageChain) { + Testing.ok(originalMessage) + } + } to "/test foo", + object : SimpleCommand(owner, "test") { + @Handler + fun CommandContext.foo(arg: MessageChain) { + Testing.ok(originalMessage) + } + } to "/test foo", + object : CompositeCommand(owner, "test") { + @SubCommand + fun CommandContext.sub(arg: MessageChain) { + Testing.ok(originalMessage) + } + } to "/test sub foo", + + object : JRawCommand(owner, "test") { + override fun onCommand(context: CommandContext, args: MessageChain) { + Testing.ok(context.originalMessage) + } + } to "/test foo", + object : JSimpleCommand(owner, "test") { + @Handler + fun foo(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage) + } + } to "/test foo", + object : JCompositeCommand(owner, "test") { + @SubCommand + fun sub(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage) + } + } to "/test sub foo", + ).map { (instance, cmd) -> + DynamicTest.dynamicTest(instance::class.supertypes.first().classifierAsKClass().simpleName) { + runBlocking { + instance.withRegistration { + assertEquals( + cmd, + Testing.withTesting { + assertSuccess(sender.executeCommand(cmd, checkPermission = false)) + }.contentToString() + ) + } + } + } + } + } + + @TestFactory + fun `can execute and get metadata`(): List { + val metadata = MyMetadata() + return listOf( + object : RawCommand(owner, "test") { + override suspend fun CommandContext.onCommand(args: MessageChain) { + Testing.ok(originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : SimpleCommand(owner, "test") { + @Handler + fun CommandContext.foo(arg: MessageChain) { + Testing.ok(originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : CompositeCommand(owner, "test") { + @SubCommand + fun CommandContext.sub(arg: MessageChain) { + Testing.ok(originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), PlainText("sub"), metadata, PlainText("foo")), + + + object : JRawCommand(owner, "test") { + override fun onCommand(context: CommandContext, args: MessageChain) { + Testing.ok(context.originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : JSimpleCommand(owner, "test") { + @Handler + fun foo(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : JCompositeCommand(owner, "test") { + @SubCommand + fun sub(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage[MyMetadata]) + } + } to messageChainOf(PlainText("/test"), PlainText("sub"), metadata, PlainText("foo")), + ).map { (instance, cmd) -> + DynamicTest.dynamicTest(instance::class.supertypes.first().classifierAsKClass().simpleName) { + runBlocking { + instance.withRegistration { + assertEquals( + metadata, + Testing.withTesting { + assertSuccess(CommandManager.executeCommand(sender, cmd, checkPermission = false)) + } + ) + } + } + } + } + } + + @TestFactory + fun `RawCommand can execute and get chain including metadata`(): List { + val metadata = MyMetadata() + return listOf( + object : RawCommand(owner, "test") { + override suspend fun CommandContext.onCommand(args: MessageChain) { + Testing.ok(originalMessage) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : SimpleCommand(owner, "test") { + @Handler + fun CommandContext.foo(arg: MessageChain) { + Testing.ok(originalMessage) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : CompositeCommand(owner, "test") { + @SubCommand + fun CommandContext.sub(arg: MessageChain) { + Testing.ok(originalMessage) + } + } to messageChainOf(PlainText("/test"), PlainText("sub"), metadata, PlainText("foo")), + + + object : JRawCommand(owner, "test") { + override fun onCommand(context: CommandContext, args: MessageChain) { + Testing.ok(context.originalMessage) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : JSimpleCommand(owner, "test") { + @Handler + fun foo(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage) + } + } to messageChainOf(PlainText("/test"), metadata, PlainText("foo")), + object : JCompositeCommand(owner, "test") { + @SubCommand + fun sub(context: CommandContext, arg: MessageChain) { + Testing.ok(context.originalMessage) + } + } to messageChainOf(PlainText("/test"), PlainText("sub"), metadata, PlainText("foo")), + ).map { (instance, cmd) -> + DynamicTest.dynamicTest(instance::class.supertypes.first().classifierAsKClass().simpleName) { + runBlocking { + instance.withRegistration { + assertEquals( + cmd, + Testing.withTesting { + assertSuccess(CommandManager.executeCommand(sender, cmd, checkPermission = false)) + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt index 3ccd1708f..71dccf4d2 100644 --- a/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt +++ b/mirai-console/backend/mirai-console/test/command/InstanceTestCommand.kt @@ -156,7 +156,7 @@ class TestTemporalArgCommand : CompositeCommand(owner, "testtemporal") { } private val sender get() = ConsoleCommandSender -internal val owner get() = ConsoleCommandOwner +private val owner get() = ConsoleCommandOwner @TestInstance(TestInstance.Lifecycle.PER_METHOD) @OptIn(ExperimentalCommandDescriptors::class)