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 f490254f1..65fd1eda2 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 @@ -64,7 +64,7 @@ interface MiraiConsole { @MiraiExperimentalAPI fun newLogger(identity: String?): MiraiLogger - companion object INSTANCE : MiraiConsole by MiraiConsoleImpl + companion object INSTANCE : MiraiConsole by MiraiConsoleInternal } @@ -77,7 +77,7 @@ internal object MiraiConsoleInitializer { /** 由前端调用 */ internal fun init(instance: IMiraiConsole) { this.instance = instance - MiraiConsoleImpl.initialize() + MiraiConsoleInternal.initialize() } } @@ -90,7 +90,7 @@ internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mira /** * mirai 控制台实例. */ -internal object MiraiConsoleImpl : CoroutineScope, IMiraiConsole, MiraiConsole { +internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConsole { override val pluginCenter: PluginCenter get() = CuiPluginCenter private val instance: IMiraiConsole 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 1f4f26668..7d7379d7a 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 @@ -11,17 +11,15 @@ package net.mamoe.mirai.console.command -import net.mamoe.mirai.console.command.description.* -import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.SingleMessage -import kotlin.reflect.KAnnotatedElement -import kotlin.reflect.KClass -import kotlin.reflect.full.* /** * 指令 * 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand * @see register 注册这个指令 + * + * @see SimpleCommand + * @see CompositeCommand */ interface Command { /** @@ -30,6 +28,7 @@ interface Command { val names: Array val usage: String + val description: String /** @@ -47,289 +46,12 @@ interface Command { /** * @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. */ - suspend fun onCommand(sender: CommandSender, args: Array) + suspend fun CommandSender.onCommand(args: Array) } +suspend inline fun Command.onCommand(sender: CommandSender, args: Array) = sender.run { onCommand(args) } + /** * 主要指令名. 为 [Command.names] 的第一个元素. */ -val Command.primaryName: String get() = names[0] - -/** - * 功能最集中的Commend - * 支持且只支持有sub的指令 - * 例: - * /mute add - * /mute remove - * /mute addandremove (sub is case insensitive, lowercase are recommend) - * /mute add and remove('add and remove' consider as a sub) - */ -abstract class CompositeCommand @JvmOverloads constructor( - override val owner: CommandOwner, - vararg names: String, - description: String = "no description available", - override val permission: CommandPermission = CommandPermission.Default, - override val prefixOptional: Boolean = false, - overrideContext: CommandParserContext = EmptyCommandParserContext -) : Command { - override val description = description.trimIndent() - override val names: Array = - names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> - list.firstOrNull { !it.isValidSubName() }?.let { - error("Name is not valid: $it") - } - }.toTypedArray() - - /** - * [CommandArgParser] 的环境 - */ - val context: CommandParserContext = CommandParserContext.Builtins + overrideContext - - override var usage: String = "" // initialized by subCommand reflection - internal set - - /** 指定子指令要求的权限 */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class Permission(val permission: KClass) - - /** 标记一个函数为子指令 */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class SubCommand(vararg val name: String) - - /** 指令描述 */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FUNCTION) - annotation class Description(val description: String) - - /** 参数名, 将参与构成 [usage] */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.VALUE_PARAMETER) - annotation class Name(val name: String) - - final override suspend fun onCommand(sender: CommandSender, args: Array) { - matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run { - defaultSubCommand.onCommand(sender, args) - } - subCommands - } - - internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { - DefaultSubCommandDescriptor( - "", - CommandPermission.Default, - onCommand = block { sender: CommandSender, args: Array -> - false//not supported yet - } - ) - } - - - class IllegalParameterException internal constructor(message: String) : Exception(message) - - internal val subCommands: Array by lazy { - - val buildUsage = StringBuilder(this.description).append(": \n") - - this@CompositeCommand::class.declaredFunctions.filter { it.hasAnnotation() }.map { function -> - - val notStatic = function.findAnnotation()==null - val overridePermission = function.findAnnotation()//optional - val subDescription = function.findAnnotation()?.description?:"no description available" - - if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) { - throw IllegalParameterException("Return Type of SubCommand must be Boolean") - } - - val parameter = function.parameters.toMutableList() - if (parameter.isEmpty()){ - throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be ") - } - - if (notStatic) { - parameter.removeAt(0) - } - - (parameter.removeAt(0)).let {receiver -> - if ( - receiver.isVararg || - ((receiver.type.classifier as? KClass<*>).also { print(it) } - ?.isSubclassOf(CommandSender::class) != true) - ) { - throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be ") - } - } - - val commandName = function.findAnnotation()!!.name.map { - if (!it.isValidSubName()) { - error("SubName $it is not valid") - } - it - }.toTypedArray() - - //map parameter - val parms = parameter.map { - buildUsage.append("/$primaryName ") - - if (it.isVararg) { - throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg") - } - if (it.isOptional) { - throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional") - } - - val argName = it.findAnnotation()?.name ?: it.name ?: "unknown" - buildUsage.append("<").append(argName).append("> ").append(" ") - CommandParam( - argName, - (it.type.classifier as? KClass<*>) - ?: throw IllegalParameterException("unsolved type reference from param " + it.name + " in " + function.name + " from " + this.primaryName) - ) - }.toTypedArray() - - buildUsage.append(subDescription).append("\n") - - SubCommandDescriptor( - commandName, - parms, - 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 - } - } - ) - - }.toTypedArray().also { - usage = buildUsage.toString() - } - } - - private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { - return block - } - - @JvmField - internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> = kotlin.run { - val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) - for (descriptor in subCommands) { - for (name in descriptor.bakedSubNames) { - map[name] = descriptor - } - } - map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) - } - - internal inner class DefaultSubCommandDescriptor( - val description: String, - val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Boolean - ) - - internal inner class SubCommandDescriptor( - val names: Array, - val params: Array>, - val description: String, - val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean - ) { - internal suspend inline fun parseAndExecute( - sender: CommandSender, - argsWithSubCommandNameNotRemoved: Array - ) { - if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) { - sender.sendMessage(usage) - } - } - - @JvmField - internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() - private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array { - require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } - - return Array(this.params.size) { index -> - val param = params[index] - val rawArg = rawArgs[offset + index] - when (rawArg) { - is String -> context[param.type]?.parse(rawArg, sender) - is SingleMessage -> context[param.type]?.parse(rawArg, sender) - else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}") - } ?: error("Cannot find a parser for $rawArg") - } - } - } - - /** - * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] - */ - internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { - val maxCount = rawArgs.size - 1 - var cur = 0 - bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> - if (name.size != cur) { - if (cur++ == maxCount) return null - } - if (name.contentEqualsOffset(rawArgs, length = cur)) { - return descriptor - } - } - return null - } -} - - -abstract class SimpleCommand( - override val owner: CommandOwner, - val name: String, - val alias: Array = arrayOf() -) : Command { - abstract suspend fun CommandSender.onCommand(args: List) -} - -abstract class RawCommand( - final override val owner: CommandOwner, - name: String, - alias: Array = arrayOf() -) : Command { - final override val names: Array = arrayOf(name, *alias) -} - - -private fun Array.contentEqualsOffset(other: Array, length: Int): Boolean { - repeat(length) { index -> - if (other[index].toString() != this[index]) { - return false - } - } - return true -} - -internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() -internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this } -internal fun String.bakeSubName(): Array = split(' ').filterNot { it.isBlank() }.toTypedArray() - -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() } - .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()) } - else -> list.add(this.toString()) - } - return list -} - -internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = - findAnnotation() != null - -internal inline fun KClass.getInstance(): T { - return this.objectInstance ?: this.createInstance() -} - +val Command.primaryName: String get() = names[0] \ No newline at end of file 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 ca7069252..fd2c39bfe 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 @@ -18,6 +18,8 @@ package net.mamoe.mirai.console.command import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import net.mamoe.mirai.console.MiraiConsoleInternal +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 @@ -50,7 +52,11 @@ abstract class PluginCommandOwner(val plugin: Plugin) : CommandOwner() { * * 由前端实现 */ -internal abstract class ConsoleCommandOwner : CommandOwner() +internal abstract class ConsoleCommandOwner : CommandOwner() { + companion object { + internal val instance get() = MiraiConsoleInternal.consoleCommandOwner + } +} /** * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. 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 b8fe33288..baa0ce513 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,18 +13,17 @@ package net.mamoe.mirai.console.command import kotlinx.coroutines.runBlocking import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.utils.JavaFriendlyAPI import net.mamoe.mirai.contact.* import net.mamoe.mirai.message.MessageEvent import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.PlainText -import org.jetbrains.annotations.Contract /** * 指令发送者 * - * @see AbstractCommandSender 请继承于该抽象类 + * @see ConsoleCommandSender + * @see UserCommandSender */ @Suppress("FunctionName") interface CommandSender { 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 new file mode 100644 index 000000000..a4ef6361f --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -0,0 +1,80 @@ +/* + * 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("EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.description.CommandArgParser +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.EmptyCommandParserContext +import net.mamoe.mirai.console.command.description.plus +import net.mamoe.mirai.console.command.internal.CompositeCommandImpl +import net.mamoe.mirai.console.command.internal.isValidSubName +import kotlin.reflect.KClass + + +/** + * 功能最集中的Commend + * 支持且只支持有sub的指令 + * 例: + * /mute add + * /mute remove + * /mute addandremove (sub is case insensitive, lowercase are recommend) + * /mute add and remove('add and remove' consider as a sub) + */ +abstract class CompositeCommand @JvmOverloads constructor( + final override val owner: CommandOwner, + vararg names: String, + description: String = "no description available", + final override val permission: CommandPermission = CommandPermission.Default, + final override val prefixOptional: Boolean = false, + overrideContext: CommandParserContext = EmptyCommandParserContext +) : Command, CompositeCommandImpl() { + final override val description = description.trimIndent() + final override val names: Array = + names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> + list.firstOrNull { !it.isValidSubName() }?.let { + error("Name is not valid: $it") + } + }.toTypedArray() + + /** + * [CommandArgParser] 的环境 + */ + val context: CommandParserContext = CommandParserContext.Builtins + overrideContext + + final override val usage: String get() = super.usage + + /** 指定子指令要求的权限 */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + annotation class Permission(val permission: KClass) + + /** 标记一个函数为子指令 */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + annotation class SubCommand(vararg val name: String) + + /** 指令描述 */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + annotation class Description(val description: String) + + /** 参数名, 将参与构成 [usage] */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.VALUE_PARAMETER) + annotation class Name(val name: String) + + final override suspend fun onCommand(sender: CommandSender, args: Array) { + matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run { + defaultSubCommand.onCommand(sender, args) + } + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt new file mode 100644 index 000000000..bd693a78c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -0,0 +1,12 @@ +package net.mamoe.mirai.console.command + +abstract class SimpleCommand( + override val owner: CommandOwner, + override vararg val names: String, + override val usage: String, + override val description: String, + override val permission: CommandPermission = CommandPermission.Default, + override val prefixOptional: Boolean = false +) : Command { + abstract override suspend fun CommandSender.onCommand(args: Array) +} 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 94acd0519..53f471854 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 @@ -20,9 +20,11 @@ import kotlin.contracts.contract * this output type of that arg * input is always String */ -abstract class CommandArgParser { - abstract fun parse(raw: String, sender: CommandSender): T - open fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender) +interface CommandArgParser { + fun parse(raw: String, sender: CommandSender): T + + @JvmDefault + fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender) } fun CommandArgParser.parse(raw: Any, sender: CommandSender): T { @@ -61,7 +63,7 @@ inline fun CommandArgParser<*>.checkArgument( @JvmSynthetic inline fun CommandArgParser( crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T -): CommandArgParser = object : CommandArgParser() { +): CommandArgParser = object : CommandArgParser { override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt index 8399965b2..49936f429 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt @@ -10,7 +10,11 @@ package net.mamoe.mirai.console.command.description import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.BotAwareCommandSender +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.MemberCommandSender +import net.mamoe.mirai.console.command.UserCommandSender +import net.mamoe.mirai.console.command.internal.fuzzySearchMember import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member 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 new file mode 100644 index 000000000..cc4a145b8 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandImpl.kt @@ -0,0 +1,219 @@ +/* + * 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("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command.internal + +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.description.CommandParam +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.SingleMessage +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.full.* + +internal abstract class CompositeCommandImpl : Command { + @JvmField + @Suppress("PropertyName") + internal var _usage: String = "" + + override val usage: String // initialized by subCommand reflection + get() = _usage + + internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { + DefaultSubCommandDescriptor( + "", + CommandPermission.Default, + onCommand = block { sender: CommandSender, args: Array -> + false//not supported yet + } + ) + } + + internal val subCommands: Array by lazy { + @Suppress("CAST_NEVER_SUCCEEDS") + this 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 " + } + } + + 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 + } + } + ) + }.toTypedArray().also { + _usage = buildUsage.toString() + } + } + + private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { + return block + } + + @JvmField + internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> = kotlin.run { + val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) + for (descriptor in subCommands) { + for (name in descriptor.bakedSubNames) { + map[name] = descriptor + } + } + map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) + } + + internal class DefaultSubCommandDescriptor( + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Boolean + ) + + internal inner class SubCommandDescriptor( + val names: Array, + val params: Array>, + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean + ) { + internal suspend inline fun parseAndExecute( + sender: CommandSender, + argsWithSubCommandNameNotRemoved: Array + ) { + if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) { + sender.sendMessage(usage) + } + } + + @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 + require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } + + return Array(this.params.size) { index -> + val param = params[index] + val rawArg = rawArgs[offset + index] + when (rawArg) { + is String -> context[param.type]?.parse(rawArg, sender) + is SingleMessage -> context[param.type]?.parse(rawArg, sender) + else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}") + } ?: error("Cannot find a parser for $rawArg") + } + } + } + + /** + * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] + */ + internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { + val maxCount = rawArgs.size - 1 + var cur = 0 + bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> + if (name.size != cur) { + if (cur++ == maxCount) return null + } + if (name.contentEqualsOffset(rawArgs, length = cur)) { + return descriptor + } + } + return null + } +} + +internal fun Array.contentEqualsOffset(other: Array, length: Int): Boolean { + repeat(length) { index -> + if (other[index].toString() != this[index]) { + return false + } + } + return true +} + +internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() +internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this } +internal fun String.bakeSubName(): Array = split(' ').filterNot { it.isBlank() }.toTypedArray() + +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() } + .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()) } + else -> list.add(this.toString()) + } + return list +} + +internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = + findAnnotation() != null + +internal inline fun KClass.getInstance(): T { + return this.objectInstance ?: this.createInstance() +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt similarity index 92% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt index f2b82aceb..a8df6ec72 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt @@ -7,8 +7,9 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.command +package net.mamoe.mirai.console.command.internal +import net.mamoe.mirai.console.command.* import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member import java.util.concurrent.locks.ReentrantLock @@ -51,7 +52,9 @@ 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 + )] } return optionalPrefixCommandMap[rawCommand] } @@ -170,10 +173,16 @@ internal suspend inline fun CommandSender.executeCommandInternal( messages: Any, commandName: String ): Command? { - val command = InternalCommandManager.matchCommand(commandName) ?: return null + val command = InternalCommandManager.matchCommand( + commandName + ) ?: return null if (!command.testPermission(this)) { - throw CommandExecutionException(command, commandName, CommandPermissionDeniedException(command)) + throw CommandExecutionException( + command, + commandName, + CommandPermissionDeniedException(command) + ) } kotlin.runCatching { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt index 3d078d520..de73126ef 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt @@ -9,16 +9,12 @@ package net.mamoe.mirai.console.event -import net.mamoe.mirai.console.command.Command -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.event.AbstractEvent -import net.mamoe.mirai.event.CancellableEvent - +/* data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent val sender: CommandSender, val command: Command, val rawArgs: Array -) : AbstractEvent(), CancellableEvent, ConsoleEvent { +) : CancellableEvent, ConsoleEvent, AbstractEvent() { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -39,3 +35,4 @@ data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent return result } } +*/ \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt index b10898d4d..48fa96095 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginImpl.kt @@ -27,6 +27,9 @@ import kotlin.coroutines.EmptyCoroutineContext internal val T.job: Job where T : CoroutineScope, T : Plugin get() = this.coroutineContext[Job]!! +/** + * Hides implementations from [JvmPlugin] + */ @PublishedApi internal abstract class JvmPluginImpl( parentCoroutineContext: CoroutineContext diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt index 6bef6b96c..8f5a75ad9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt @@ -25,4 +25,6 @@ abstract class AbstractJvmPlugin @JvmOverloads constructor( parentCoroutineContext: CoroutineContext = EmptyCoroutineContext ) : JvmPlugin, JvmPluginImpl(parentCoroutineContext) { // TODO: 2020/6/24 添加 PluginSetting 继承 Setting, 实现 onValueChanged 并绑定自动保存. + + abstract class PluginSetting } \ No newline at end of file