mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-25 15:40:28 +08:00
Add command test, fix various command bugs
This commit is contained in:
parent
b9342be382
commit
32e77ca420
@ -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<PluginLoader<*, *>>
|
||||
|
||||
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
|
||||
internal val consoleCommandOwner: ConsoleCommandOwner
|
||||
|
||||
@Suppress("WRONG_MODIFIER_CONTAINING_DECLARATION")
|
||||
internal val consoleCommandSender: ConsoleCommandSender
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.SingleMessage
|
||||
* 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand
|
||||
* @see register 注册这个指令
|
||||
*
|
||||
* @see SimpleCommand
|
||||
* @see RawCommand
|
||||
* @see CompositeCommand
|
||||
*/
|
||||
interface Command {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -55,10 +55,10 @@ abstract class CompositeCommand @JvmOverloads constructor(
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class Permission(val permission: KClass<out CommandPermission>)
|
||||
|
||||
/** 标记一个函数为子指令 */
|
||||
/** 标记一个函数为子指令, 当 [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<out Any>) {
|
||||
sendMessage(usage)
|
||||
}
|
||||
|
||||
final override suspend fun CommandSender.onCommand(args: Array<out Any>) {
|
||||
matchSubCommand(args)?.parseAndExecute(this, args) ?: kotlin.run {
|
||||
defaultSubCommand.onCommand(this, args)
|
||||
|
@ -62,9 +62,22 @@ inline fun CommandArgParser<*>.checkArgument(
|
||||
@Suppress("FunctionName")
|
||||
@JvmSynthetic
|
||||
inline fun <T : Any> CommandArgParser(
|
||||
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
||||
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
||||
): CommandArgParser<T> = object : CommandArgParser<T> {
|
||||
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 <T : Any> CommandArgParser(
|
||||
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T,
|
||||
crossinline messageParser: CommandArgParser<T>.(m: SingleMessage, sender: CommandSender) -> T
|
||||
): CommandArgParser<T> = object : CommandArgParser<T> {
|
||||
override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender)
|
||||
override fun parse(raw: SingleMessage, sender: CommandSender): T = messageParser(raw, sender)
|
||||
}
|
||||
|
||||
|
||||
|
@ -127,9 +127,6 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
|
||||
inline infix fun <T : Any> KClass<T>.with(parser: CommandArgParser<T>): ParserPair<*> =
|
||||
ParserPair(this, parser).also { add(it) }
|
||||
|
||||
inline infix fun <reified T : Any> auto(parser: CommandArgParser<T>): ParserPair<*> =
|
||||
ParserPair(T::class, parser).also { add(it) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@ -147,12 +144,16 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
|
||||
crossinline parser: CommandArgParser<T>.(s: String) -> T
|
||||
): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }).also { add(it) }
|
||||
|
||||
@JvmSynthetic
|
||||
inline fun <reified T : Any> add(parser: CommandArgParser<T>): ParserPair<*> =
|
||||
ParserPair(T::class, parser).also { add(it) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@MiraiExperimentalAPI
|
||||
@JvmSynthetic
|
||||
inline infix fun <reified T : Any> auto(
|
||||
inline infix fun <reified T : Any> add(
|
||||
crossinline parser: CommandArgParser<*>.(s: String) -> T
|
||||
): ParserPair<*> = T::class with CommandArgParser { s: String, _: CommandSender -> parser(s) }
|
||||
|
||||
@ -162,7 +163,7 @@ class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf(
|
||||
@MiraiExperimentalAPI
|
||||
@JvmSynthetic
|
||||
@LowPriorityInOverloadResolution
|
||||
inline infix fun <reified T : Any> auto(
|
||||
inline infix fun <reified T : Any> add(
|
||||
crossinline parser: CommandArgParser<*>.(s: String, sender: CommandSender) -> T
|
||||
): ParserPair<*> = T::class with CommandArgParser(parser)
|
||||
}
|
||||
|
@ -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<out Any>)
|
||||
|
||||
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
|
||||
DefaultSubCommandDescriptor(
|
||||
"",
|
||||
CommandPermission.Default,
|
||||
onCommand = block { sender: CommandSender, args: Array<out Any> ->
|
||||
false//not supported yet
|
||||
onCommand = block2 { sender: CommandSender, args: Array<out Any> ->
|
||||
sender.onDefault(args)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal val subCommands: Array<SubCommandDescriptor> 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<CompositeCommand.SubCommand>() }.map { function ->
|
||||
val notStatic = !function.hasAnnotation<JvmStatic>()
|
||||
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
|
||||
val subDescription =
|
||||
function.findAnnotation<CompositeCommand.Description>()?.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 <out CommandSender>"
|
||||
}
|
||||
|
||||
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 <out CommandSender>"
|
||||
this::class.declaredFunctions.filter { it.hasAnnotation<CompositeCommand.SubCommand>() }
|
||||
.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<JvmStatic>()
|
||||
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
|
||||
val subDescription =
|
||||
function.findAnnotation<CompositeCommand.Description>()?.description ?: "<no description available>"
|
||||
|
||||
val commandName = function.findAnnotation<CompositeCommand.SubCommand>()!!.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<CompositeCommand.Name>()?.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<out Any> ->
|
||||
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<CompositeCommand.SubCommand>()!!.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<CompositeCommand.Name>()?.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<out Any> ->
|
||||
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<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
|
||||
return block
|
||||
}
|
||||
|
||||
private fun block2(block: suspend (CommandSender, Array<out Any>) -> Unit): suspend (CommandSender, Array<out Any>) -> Unit {
|
||||
return block
|
||||
}
|
||||
|
||||
internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> by lazy {
|
||||
kotlin.run {
|
||||
val map = LinkedHashMap<Array<String>, 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<out Any>) -> Boolean
|
||||
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Unit
|
||||
)
|
||||
|
||||
internal inner class SubCommandDescriptor(
|
||||
val names: Array<String>,
|
||||
val names: Array<out String>,
|
||||
val params: Array<CommandParam<*>>,
|
||||
val description: String,
|
||||
val permission: CommandPermission,
|
||||
@ -151,8 +197,7 @@ internal abstract class CompositeCommandImpl : Command {
|
||||
@JvmField
|
||||
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
|
||||
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
|
||||
@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<String> = split(' ').filterNot { it.isB
|
||||
|
||||
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
|
||||
val list = ArrayList<Any>()
|
||||
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<Any>::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 <reified T : Annotation> KAnnotatedElement.hasAnnotation():
|
||||
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
|
||||
return this.objectInstance ?: this.createInstance()
|
||||
}
|
||||
|
||||
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
|
||||
|
@ -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 <T> Array<out T>.intersects(other: Array<out T>): Boolean {
|
||||
internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): 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 <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = 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<out Any>,
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<PluginLoader<*, *>> = 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<Any?>? = null
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R {
|
||||
@Suppress("RemoveExplicitTypeArguments") // bug
|
||||
return if (timeout != -1L) {
|
||||
withTimeout<R>(timeout) {
|
||||
suspendCancellableCoroutine<R> { ct ->
|
||||
this@Testing.cont = ct as Continuation<Any?>
|
||||
runBlocking { block() }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
suspendCancellableCoroutine<R> { ct ->
|
||||
this.cont = ct as Continuation<Any?>
|
||||
runBlocking { block() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ok(result: Any? = Unit) {
|
||||
val cont = cont
|
||||
assertNotNull(cont)
|
||||
cont.resume(result)
|
||||
}
|
||||
}
|
@ -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<out Any>) {
|
||||
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<Array<String>> {
|
||||
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<Array<String>> {
|
||||
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<Array<Any>> {
|
||||
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<Array<String>> {
|
||||
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<IllegalStateException> {
|
||||
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<MyClass> {
|
||||
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<MyClass> { execute(sender, "mute 333") }.value)
|
||||
assertEquals(2, withTesting<MyClass> { execute(sender, "mute", image) }.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 : Command, R> T.withRegistration(block: T.() -> R): R {
|
||||
this.register()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
this.unregister()
|
||||
}
|
||||
}
|
@ -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<Long>) {
|
||||
}
|
||||
|
||||
override fun createLoginSolver(): LoginSolver {
|
||||
return DefaultLoginSolver(
|
||||
input = suspend {
|
||||
|
Loading…
Reference in New Issue
Block a user