diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index c5988ece7..75d81047d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION") + package net.mamoe.mirai.console import kotlinx.coroutines.CoroutineScope @@ -153,10 +155,8 @@ internal interface IMiraiConsole : CoroutineScope { */ val builtInPluginLoaders: List> - @Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION") internal val consoleCommandOwner: ConsoleCommandOwner - @Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION") internal val consoleCommandSender: ConsoleCommandSender } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index 7d7379d7a..237dbb2c1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.SingleMessage * 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand * @see register 注册这个指令 * - * @see SimpleCommand + * @see RawCommand * @see CompositeCommand */ interface Command { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt index fd2c39bfe..bd0271000 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -23,7 +23,6 @@ import net.mamoe.mirai.console.command.internal.* import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.utils.MiraiInternalAPI /** * 指令的所有者. @@ -31,9 +30,6 @@ import net.mamoe.mirai.utils.MiraiInternalAPI */ sealed class CommandOwner -@MiraiInternalAPI -object TestCommandOwner : CommandOwner() - /** * 插件指令所有者. 插件只能通过 [PluginCommandOwner] 管理指令. */ @@ -99,22 +95,29 @@ fun CommandOwner.unregisterAllCommands() { * @see JCommandManager.register Java 方法 */ @JvmOverloads -fun Command.register(override: Boolean = false): Boolean = InternalCommandManager.modifyLock.withLock { - if (!override) { - if (findDuplicate() != null) return false - } - InternalCommandManager.registeredCommands.add(this@register) - if (this.prefixOptional) { - for (name in this.names) { - InternalCommandManager.optionalPrefixCommandMap[name] = this +fun Command.register(override: Boolean = false): Boolean { + if (this is CompositeCommand) this.subCommands // init + + InternalCommandManager.modifyLock.withLock { + if (!override) { + if (findDuplicate() != null) return false } - } else { - for (name in this.names) { - InternalCommandManager.optionalPrefixCommandMap.remove(name) // ensure resolution consistency - InternalCommandManager.requiredPrefixCommandMap[name] = this + InternalCommandManager.registeredCommands.add(this@register) + if (this.prefixOptional) { + for (name in this.names) { + val lowerCaseName = name.toLowerCase() + InternalCommandManager.optionalPrefixCommandMap[lowerCaseName] = this + InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this + } + } else { + for (name in this.names) { + val lowerCaseName = name.toLowerCase() + InternalCommandManager.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency + InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this + } } + return true } - return true } /** @@ -123,7 +126,7 @@ fun Command.register(override: Boolean = false): Boolean = InternalCommandManage * @see JCommandManager.findDuplicate Java 方法 */ fun Command.findDuplicate(): Command? = - InternalCommandManager.registeredCommands.firstOrNull { it.names intersects this.names } + InternalCommandManager.registeredCommands.firstOrNull { it.names intersectsIgnoringCase this.names } /** * 取消注册这个指令. 若指令未注册, 返回 `false`. @@ -131,9 +134,22 @@ fun Command.findDuplicate(): Command? = * @see JCommandManager.unregister Java 方法 */ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { + if (this.prefixOptional) { + this.names.forEach { + InternalCommandManager.optionalPrefixCommandMap.remove(it) + } + } + this.names.forEach { + InternalCommandManager.requiredPrefixCommandMap.remove(it) + } InternalCommandManager.registeredCommands.remove(this) } +/** + * 当 [this] 已经 [注册][register] 后返回 `true` + */ +fun Command.isRegistered(): Boolean = this in InternalCommandManager.registeredCommands + //// executing without detailed result (faster) /** @@ -148,7 +164,7 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { */ suspend fun CommandSender.executeCommand(vararg messages: Any): Command? { if (messages.isEmpty()) return null - return executeCommandInternal(messages, messages[0].toString().substringBefore(' ')) + return matchAndExecuteCommandInternal(messages, messages[0].toString().substringBefore(' ')) } /** @@ -162,9 +178,46 @@ suspend fun CommandSender.executeCommand(vararg messages: Any): Command? { @Throws(CommandExecutionException::class) suspend fun CommandSender.executeCommand(message: MessageChain): Command? { if (message.isEmpty()) return null - return executeCommandInternal(message, message[0].toString()) + return matchAndExecuteCommandInternal(message, message[0].toString().substringBefore(' ')) } +/** + * 执行一个指令 + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +@JvmOverloads +@Throws(CommandExecutionException::class) +suspend fun Command.execute(sender: CommandSender, args: MessageChain, checkPermission: Boolean = true) { + sender.executeCommandInternal( + this, + args.flattenCommandComponents().toTypedArray(), + this.primaryName, + checkPermission + ) +} + +/** + * 执行一个指令 + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +@JvmOverloads +@Throws(CommandExecutionException::class) +suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean = true) { + sender.executeCommandInternal( + this, + args.flattenCommandComponents().toTypedArray(), + this.primaryName, + checkPermission + ) +} //// execution with detailed result diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index baa0ce513..e0dc664fe 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -13,6 +13,7 @@ package net.mamoe.mirai.console.command import kotlinx.coroutines.runBlocking import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsoleInternal import net.mamoe.mirai.console.utils.JavaFriendlyAPI import net.mamoe.mirai.contact.* import net.mamoe.mirai.message.MessageEvent @@ -64,6 +65,10 @@ suspend inline fun CommandSender.sendMessage(message: String) = sendMessage(Plai // 前端实现 abstract class ConsoleCommandSender internal constructor() : CommandSender { final override val bot: Nothing? get() = null + + companion object { + internal val instance get() = MiraiConsoleInternal.consoleCommandSender + } } fun Friend.asCommandSender(): FriendCommandSender = FriendCommandSender(this) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt index d797c9ae5..6af46e2ed 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -55,10 +55,10 @@ abstract class CompositeCommand @JvmOverloads constructor( @Target(AnnotationTarget.FUNCTION) annotation class Permission(val permission: KClass) - /** 标记一个函数为子指令 */ + /** 标记一个函数为子指令, 当 [names] 为空时使用函数名. */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) - annotation class SubCommand(vararg val name: String) + annotation class SubCommand(vararg val names: String) /** 指令描述 */ @Retention(AnnotationRetention.RUNTIME) @@ -70,6 +70,10 @@ abstract class CompositeCommand @JvmOverloads constructor( @Target(AnnotationTarget.VALUE_PARAMETER) annotation class Name(val name: String) + public override suspend fun CommandSender.onDefault(rawArgs: Array) { + sendMessage(usage) + } + final override suspend fun CommandSender.onCommand(args: Array) { matchSubCommand(args)?.parseAndExecute(this, args) ?: kotlin.run { defaultSubCommand.onCommand(this, args) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt index 53f471854..737fd8c5d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt @@ -62,9 +62,22 @@ inline fun CommandArgParser<*>.checkArgument( @Suppress("FunctionName") @JvmSynthetic inline fun CommandArgParser( - crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T + crossinline stringParser: CommandArgParser.(s: String, sender: CommandSender) -> T ): CommandArgParser = object : CommandArgParser { - override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) + override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender) +} + +/** + * 创建匿名 [CommandArgParser] + */ +@Suppress("FunctionName") +@JvmSynthetic +inline fun CommandArgParser( + crossinline stringParser: CommandArgParser.(s: String, sender: CommandSender) -> T, + crossinline messageParser: CommandArgParser.(m: SingleMessage, sender: CommandSender) -> T +): CommandArgParser = object : CommandArgParser { + override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender) + override fun parse(raw: SingleMessage, sender: CommandSender): T = messageParser(raw, sender) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt index e771ee284..3b3a3f127 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt @@ -127,9 +127,6 @@ class CommandParserContextBuilder : MutableList> by mutableListOf( inline infix fun KClass.with(parser: CommandArgParser): ParserPair<*> = ParserPair(this, parser).also { add(it) } - inline infix fun auto(parser: CommandArgParser): ParserPair<*> = - ParserPair(T::class, parser).also { add(it) } - /** * 添加一个指令解析器 */ @@ -147,12 +144,16 @@ class CommandParserContextBuilder : MutableList> by mutableListOf( crossinline parser: CommandArgParser.(s: String) -> T ): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }).also { add(it) } + @JvmSynthetic + inline fun add(parser: CommandArgParser): ParserPair<*> = + ParserPair(T::class, parser).also { add(it) } + /** * 添加一个指令解析器 */ @MiraiExperimentalAPI @JvmSynthetic - inline infix fun auto( + inline infix fun add( crossinline parser: CommandArgParser<*>.(s: String) -> T ): ParserPair<*> = T::class with CommandArgParser { s: String, _: CommandSender -> parser(s) } @@ -162,7 +163,7 @@ class CommandParserContextBuilder : MutableList> by mutableListOf( @MiraiExperimentalAPI @JvmSynthetic @LowPriorityInOverloadResolution - inline infix fun auto( + inline infix fun add( crossinline parser: CommandArgParser<*>.(s: String, sender: CommandSender) -> T ): ParserPair<*> = T::class with CommandArgParser(parser) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt index 8e52c9e2e..0c2cfeccc 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt @@ -27,93 +27,139 @@ internal abstract class CompositeCommandImpl : Command { override val usage: String // initialized by subCommand reflection get() = _usage + internal abstract suspend fun CommandSender.onDefault(rawArgs: Array) + internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { DefaultSubCommandDescriptor( "", CommandPermission.Default, - onCommand = block { sender: CommandSender, args: Array -> - false//not supported yet + onCommand = block2 { sender: CommandSender, args: Array -> + sender.onDefault(args) } ) } internal val subCommands: Array by lazy { - @Suppress("CAST_NEVER_SUCCEEDS") - this as CompositeCommand + this@CompositeCommandImpl as CompositeCommand val buildUsage = StringBuilder(this.description).append(": \n") - this::class.declaredFunctions.filter { it.hasAnnotation() }.map { function -> - val notStatic = !function.hasAnnotation() - val overridePermission = function.findAnnotation()//optional - val subDescription = - function.findAnnotation()?.description ?: "no description available" - - if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) { - error("Return Type of SubCommand must be Boolean") - } - - val parameters = function.parameters.toMutableList() - check(parameters.isNotEmpty()) { - "First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be " - } - - if (notStatic) parameters.removeAt(0) // instance - - (parameters.removeAt(0)).let { receiver -> - check(!receiver.isVararg && !((receiver.type.classifier as? KClass<*>).also { print(it) } - ?.isSubclassOf(CommandSender::class) != true)) { - "First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be " + this::class.declaredFunctions.filter { it.hasAnnotation() } + .also { subCommandFunctions -> + // overloading not yet supported + val overloadFunction = subCommandFunctions.groupBy { it.name }.entries.firstOrNull { it.value.size > 1 } + if (overloadFunction != null) { + error("Sub command overloading is not yet supported. (at ${this::class.qualifiedNameOrTip}.${overloadFunction.key})") } - } + }.map { function -> + val notStatic = !function.hasAnnotation() + val overridePermission = function.findAnnotation()//optional + val subDescription = + function.findAnnotation()?.description ?: "" - val commandName = function.findAnnotation()!!.name.map { - if (!it.isValidSubName()) { - error("SubName $it is not valid") - } - it - }.toTypedArray() - - //map parameter - val params = parameters.map { param -> - buildUsage.append("/$primaryName ") - - if (param.isVararg) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg") - if (param.isOptional) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional") - - val argName = param.findAnnotation()?.name ?: param.name ?: "unknown" - buildUsage.append("<").append(argName).append("> ").append(" ") - CommandParam( - argName, - (param.type.classifier as? KClass<*>) - ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + " in " + function.name + " from " + this.primaryName) - ) - }.toTypedArray() - - buildUsage.append(subDescription).append("\n") - - SubCommandDescriptor( - commandName, - params, - subDescription, - overridePermission?.permission?.getInstance() ?: permission, - onCommand = block { sender: CommandSender, args: Array -> - if (notStatic) { - function.callSuspend(this, sender, *args) as Boolean - } else { - function.callSuspend(sender, *args) as Boolean + fun KClass<*>.isValidReturnType(): Boolean { + return when (this) { + Boolean::class, Void::class, Unit::class, Nothing::class -> true + else -> false } } - ) - }.toTypedArray().also { - _usage = buildUsage.toString() - } + + check((function.returnType.classifier as? KClass<*>)?.isValidReturnType() == true) { + error("Return type of sub command ${function.name} must be one of the following: kotlin.Boolean, java.lang.Boolean, kotlin.Unit (including implicit), kotlin.Nothing, boolean or void (at ${this::class.qualifiedNameOrTip}.${function.name})") + } + + check(!function.returnType.isMarkedNullable) { + error("Return type of sub command ${function.name} must not be marked nullable in Kotlin, and must be marked with @NotNull or @NonNull explicitly in Java. (at ${this::class.qualifiedNameOrTip}.${function.name})") + } + + val parameters = function.parameters.toMutableList() + + if (notStatic) parameters.removeAt(0) // instance + + var hasSenderParam = false + check(parameters.isNotEmpty()) { + "Parameters of sub command ${function.name} must not be empty. (Must have CommandSender as its receiver or first parameter or absent, followed by naturally typed params) (at ${this::class.qualifiedNameOrTip}.${function.name})" + } + + parameters.forEach { param -> + check(!param.isVararg) { + "Parameter $param must not be vararg. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)" + } + } + + (parameters.first()).let { receiver -> + if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) { + hasSenderParam = true + parameters.removeAt(0) + } + } + + val commandName = + function.findAnnotation()!!.names + .let { namesFromAnnotation -> + if (namesFromAnnotation.isNotEmpty()) { + namesFromAnnotation + } else arrayOf(function.name) + }.also { names -> + names.forEach { + check(it.isValidSubName()) { + "Name of sub command ${function.name} is invalid" + } + } + } + + //map parameter + val params = parameters.map { param -> + buildUsage.append("/$primaryName ") + + if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + + val argName = param.findAnnotation()?.name ?: param.name ?: "unknown" + buildUsage.append("<").append(argName).append("> ").append(" ") + CommandParam( + argName, + (param.type.classifier as? KClass<*>) + ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + ) + }.toTypedArray() + + buildUsage.append(subDescription).append("\n") + + SubCommandDescriptor( + commandName, + params, + subDescription, + overridePermission?.permission?.getInstance() ?: permission, + onCommand = block { sender: CommandSender, args: Array -> + val result = if (notStatic) { + if (hasSenderParam) { + function.isSuspend + function.callSuspend(this, sender, *args) + } else function.callSuspend(this, *args) + } else { + if (hasSenderParam) { + function.callSuspend(sender, *args) + } else function.callSuspend(*args) + } + + checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" } + + result as? Boolean ?: true // Unit, void is considered as true. + } + ) + }.toTypedArray().also { + _usage = buildUsage.toString() + } } private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { return block } + private fun block2(block: suspend (CommandSender, Array) -> Unit): suspend (CommandSender, Array) -> Unit { + return block + } + internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> by lazy { kotlin.run { val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) @@ -129,11 +175,11 @@ internal abstract class CompositeCommandImpl : Command { internal class DefaultSubCommandDescriptor( val description: String, val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Boolean + val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Unit ) internal inner class SubCommandDescriptor( - val names: Array, + val names: Array, val params: Array>, val description: String, val permission: CommandPermission, @@ -151,8 +197,7 @@ internal abstract class CompositeCommandImpl : Command { @JvmField internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array { - @Suppress("CAST_NEVER_SUCCEEDS") - this as CompositeCommand + this@CompositeCommandImpl as CompositeCommand require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } return Array(this.params.size) { index -> @@ -200,13 +245,13 @@ internal fun String.bakeSubName(): Array = split(' ').filterNot { it.isB internal fun Any.flattenCommandComponents(): ArrayList { val list = ArrayList() - when (this::class.java) { // faster than is - String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } - PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() } + when (this) { + is PlainText -> this.content.splitToSequence(' ').filterNot { it.isBlank() } .forEach { list.add(it) } - SingleMessage::class.java -> list.add(this as SingleMessage) - Array::class.java -> (this as Array<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } - Iterable::class.java -> (this as Iterable<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + is CharSequence -> this.splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } + is SingleMessage -> list.add(this) + is Array<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + is Iterable<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } else -> list.add(this.toString()) } return list @@ -218,3 +263,5 @@ internal inline fun KAnnotatedElement.hasAnnotation(): internal inline fun KClass.getInstance(): T { return this.objectInstance ?: this.createInstance() } + +internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt index a8df6ec72..d62a7902a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt @@ -52,24 +52,20 @@ internal object InternalCommandManager { */ internal fun matchCommand(rawCommand: String): Command? { if (rawCommand.startsWith(COMMAND_PREFIX)) { - return requiredPrefixCommandMap[rawCommand.substringAfter( - COMMAND_PREFIX - )] + return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX).toLowerCase()] } - return optionalPrefixCommandMap[rawCommand] + return optionalPrefixCommandMap[rawCommand.toLowerCase()] } } -internal infix fun Array.intersects(other: Array): Boolean { +internal infix fun Array.intersectsIgnoringCase(other: Array): Boolean { val max = this.size.coerceAtMost(other.size) for (i in 0 until max) { - if (this[i] == other[i]) return true + if (this[i].equals(other[i], ignoreCase = true)) return true } return false } - - internal fun String.fuzzyCompare(target: String): Double { var step = 0 if (this == target) { @@ -169,7 +165,7 @@ internal inline fun List.dropToTypedArray(n: Int): Array = Arr @JvmSynthetic @Throws(CommandExecutionException::class) -internal suspend inline fun CommandSender.executeCommandInternal( +internal suspend inline fun CommandSender.matchAndExecuteCommandInternal( messages: Any, commandName: String ): Command? { @@ -177,7 +173,19 @@ internal suspend inline fun CommandSender.executeCommandInternal( commandName ) ?: return null - if (!command.testPermission(this)) { + this.executeCommandInternal(command, messages.flattenCommandComponents().dropToTypedArray(1), commandName, true) + return command +} + +@JvmSynthetic +@Throws(CommandExecutionException::class) +internal suspend inline fun CommandSender.executeCommandInternal( + command: Command, + args: Array, + commandName: String, + checkPermission: Boolean +) { + if (checkPermission && !command.testPermission(this)) { throw CommandExecutionException( command, commandName, @@ -186,13 +194,8 @@ internal suspend inline fun CommandSender.executeCommandInternal( } kotlin.runCatching { - command.onCommand(this, messages.flattenCommandComponents().dropToTypedArray(1)) - }.fold( - onSuccess = { - return command - }, - onFailure = { - throw CommandExecutionException(command, commandName, it) - } - ) -} + command.onCommand(this, args) + }.onFailure { + throw CommandExecutionException(command, commandName, it) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt new file mode 100644 index 000000000..fc1414ae3 --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt @@ -0,0 +1,77 @@ +/* + * 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 + */ + +package net.mamoe.mirai.console + +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.ConsoleCommandOwner +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.utils.DefaultLogger +import net.mamoe.mirai.utils.LoginSolver +import net.mamoe.mirai.utils.MiraiLogger +import java.io.File +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.test.assertNotNull + +fun initTestEnvironment() { + MiraiConsoleInitializer.init(object : IMiraiConsole { + override val rootDir: File = createTempDir() + override val frontEnd: MiraiConsoleFrontEnd = object : MiraiConsoleFrontEnd { + override fun loggerFor(identity: String?): MiraiLogger = DefaultLogger(identity) + override fun pushBot(bot: Bot) = println("pushBot: $bot") + override suspend fun requestInput(hint: String): String = readLine()!! + override fun createLoginSolver(): LoginSolver = LoginSolver.Default + } + override val mainLogger: MiraiLogger = DefaultLogger("main") + override val builtInPluginLoaders: List> = listOf(JarPluginLoader) + override val consoleCommandOwner: ConsoleCommandOwner = object : ConsoleCommandOwner() {} + override val consoleCommandSender: ConsoleCommandSender = object : ConsoleCommandSender() { + override suspend fun sendMessage(message: Message) = println(message) + } + override val coroutineContext: CoroutineContext = SupervisorJob() + }) +} + +internal object Testing { + @Volatile + internal var cont: Continuation? = null + + @Suppress("UNCHECKED_CAST") + suspend fun withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R { + @Suppress("RemoveExplicitTypeArguments") // bug + return if (timeout != -1L) { + withTimeout(timeout) { + suspendCancellableCoroutine { ct -> + this@Testing.cont = ct as Continuation + runBlocking { block() } + } + } + } else { + suspendCancellableCoroutine { ct -> + this.cont = ct as Continuation + runBlocking { block() } + } + } + } + + fun ok(result: Any? = Unit) { + val cont = cont + assertNotNull(cont) + cont.resume(result) + } +} diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt new file mode 100644 index 000000000..3e219ac55 --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -0,0 +1,193 @@ +/* + * 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") + +package net.mamoe.mirai.console.command + +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.console.Testing +import net.mamoe.mirai.console.Testing.withTesting +import net.mamoe.mirai.console.command.description.CommandArgParser +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.internal.InternalCommandManager +import net.mamoe.mirai.console.command.internal.flattenCommandComponents +import net.mamoe.mirai.console.initTestEnvironment +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.toMessage +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import kotlin.test.* + +object TestCompositeCommand : CompositeCommand( + ConsoleCommandOwner.instance, + "testComposite", "tsC" +) { + @SubCommand + fun mute(seconds: Int) { + Testing.ok(seconds) + } +} + +object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") { + override suspend fun CommandSender.onCommand(args: Array) { + Testing.ok(args) + } +} + +internal val sender by lazy { ConsoleCommandSender.instance } +internal val owner by lazy { ConsoleCommandOwner.instance } + +internal class TestCommand { + companion object { + @JvmStatic + @BeforeAll + fun init() { + initTestEnvironment() + } + } + + @Test + fun testRegister() { + try { + assertTrue(TestCompositeCommand.register()) + assertFalse(TestCompositeCommand.register()) + + assertEquals(1, ConsoleCommandOwner.instance.registeredCommands.size) + + assertEquals(1, InternalCommandManager.registeredCommands.size) + assertEquals(1, InternalCommandManager.requiredPrefixCommandMap.size) + } finally { + TestCompositeCommand.unregister() + } + } + + @Test + fun testSimpleExecute() = runBlocking { + assertEquals(arrayOf("test").contentToString(), withTesting> { + TestSimpleCommand.execute(sender, "test") + }.contentToString()) + } + + @Test + fun `test flattenCommandArgs`() { + val result = arrayOf("test", image).flattenCommandComponents().toTypedArray() + + assertEquals("test", result[0]) + assertSame(image, result[1]) + + assertEquals(2, result.size) + } + + @Test + fun testSimpleArgsSplitting() = runBlocking { + assertEquals(arrayOf("test", "ttt", "tt").contentToString(), withTesting> { + TestSimpleCommand.execute(sender, "test ttt tt".toMessage()) + }.contentToString()) + } + + val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") + + @Test + fun `PlainText and Image args splitting`() = runBlocking { + val result = withTesting> { + TestSimpleCommand.execute(sender, "test", image, "tt") + } + assertEquals(arrayOf("test", image, "tt").contentToString(), result.contentToString()) + assertSame(image, result[1]) + } + + @Test + fun `test throw Exception`() = runBlocking { + assertEquals(null, sender.executeCommand("")) + } + + @Test + fun `executing command by string command`() = runBlocking { + TestCompositeCommand.register() + val result = withTesting> { + assertNotNull(sender.executeCommand("testComposite", "test")) + } + + assertEquals("test", result.single()) + } + + @Test + fun `composite command executing`() = runBlocking { + assertEquals(1, withTesting { + assertNotNull(TestCompositeCommand.execute(sender, "mute 1")) + }) + } + + @Test + fun `composite sub command resolution conflict`() { + runBlocking { + val composite = object : CompositeCommand( + ConsoleCommandOwner.instance, + "tr" + ) { + @SubCommand + fun mute(seconds: Int) { + Testing.ok(1) + } + + @SubCommand + fun mute(seconds: Int, arg2: Int) { + Testing.ok(2) + } + } + + assertFailsWith { + composite.register() + } + /* + composite.withRegistration { + assertEquals(1, withTesting { execute(sender, "tr", "mute 123") }) // one args, resolves to mute(Int) + assertEquals(2, withTesting { execute(sender, "tr", "mute 123 123") }) + }*/ + } + } + + @Test + fun `composite sub command parsing`() { + runBlocking { + class MyClass( + val value: Int + ) + + val composite = object : CompositeCommand( + ConsoleCommandOwner.instance, + "test", + overrideContext = CommandParserContext { + add(object : CommandArgParser { + override fun parse(raw: String, sender: CommandSender): MyClass { + return MyClass(raw.toInt()) + } + + override fun parse(raw: SingleMessage, sender: CommandSender): MyClass { + assertSame(image, raw) + return MyClass(2) + } + }) + } + ) { + @SubCommand + fun mute(seconds: MyClass) { + Testing.ok(seconds) + } + } + + composite.withRegistration { + assertEquals(333, withTesting { execute(sender, "mute 333") }.value) + assertEquals(2, withTesting { execute(sender, "mute", image) }.value) + } + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt deleted file mode 100644 index 2494c0de7..000000000 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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") - -package net.mamoe.mirai.console.command - -import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.message.data.Image -import org.junit.jupiter.api.Test - -object TestCompositeCommand : CompositeCommand( - TestCommandOwner, - "groupManagement", "grpMgn" -) { - @SubCommand - suspend fun CommandSender.mute(image: Image, target: Member, seconds: Int): Boolean { - target.mute(seconds) - return true - } -} - - -internal class TestComposite { - - @Test - fun testRegister() { - TestCompositeCommand.register() - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt new file mode 100644 index 000000000..79fb5e58f --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt @@ -0,0 +1,19 @@ +/* + * 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 + */ + +package net.mamoe.mirai.console.command + +inline fun T.withRegistration(block: T.() -> R): R { + this.register() + try { + return block() + } finally { + this.unregister() + } +} \ No newline at end of file diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt index e8dc2ef2e..cc444d5ca 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt @@ -62,9 +62,6 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd { return globalLogger } - override fun prePushBot(identity: Long) { - } - override fun pushBot(bot: Bot) { } @@ -80,9 +77,6 @@ object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd { return ConsoleUtils.lineReader.readLine("> ") } - override fun pushBotAdminStatus(identity: Long, admins: List) { - } - override fun createLoginSolver(): LoginSolver { return DefaultLoginSolver( input = suspend {