Review command

This commit is contained in:
Him188 2020-10-24 21:19:50 +08:00
parent d10f2b4bea
commit b9580ffcbd
7 changed files with 212 additions and 155 deletions

View File

@ -11,7 +11,6 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware
import net.mamoe.mirai.console.command.descriptor.CommandSignatureVariant
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
@ -25,7 +24,7 @@ import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* 指令
*
* @see CommandManager.register 注册这个指令
* @see CommandManager.registerCommand 注册这个指令
*
* @see RawCommand 无参数解析, 接收原生参数的指令
* @see CompositeCommand 复合指令
@ -52,7 +51,7 @@ public interface Command {
public val secondaryNames: Array<out String>
/**
*
* 指令可能的参数列表.
*/
@ConsoleExperimentalApi("Property name is experimental")
@ExperimentalCommandDescriptors
@ -64,12 +63,12 @@ public interface Command {
public val usage: String
/**
* 指令描述, 用于显示在 [BuiltInCommands.HelpCommand]
* 描述, 用于显示在 [BuiltInCommands.HelpCommand]
*/
public val description: String
/**
* 此指令分配的权限.
* 此指令分配的权限.
*
* ### 实现约束
* - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace]
@ -82,6 +81,8 @@ public interface Command {
*
* 会影响聊天语境中的解析.
*/
@ExperimentalCommandDescriptors
@ConsoleExperimentalApi
public val prefixOptional: Boolean
/**
@ -109,7 +110,7 @@ public interface Command {
public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) {
when {
name.isBlank() -> throw IllegalArgumentException("Command name should not be blank.")
name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in command name.")
name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in command name.")
name.contains(':') -> throw IllegalArgumentException("':' is forbidden in command name.")
name.contains('.') -> throw IllegalArgumentException("'.' is forbidden in command name.")
}

View File

@ -20,27 +20,17 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandCallParser
import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall
import net.mamoe.mirai.console.command.resolve.CommandCallResolver
import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall
import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission
import net.mamoe.mirai.console.internal.command.executeCommandImpl
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.*
/**
* 指令管理器
*/
public interface CommandManager {
/**
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
*
* @return 这一时刻的浅拷贝.
*/
public val CommandOwner.registeredCommands: List<Command>
/**
* 获取所有已经注册了指令列表.
*
@ -54,9 +44,17 @@ public interface CommandManager {
public val commandPrefix: String
/**
* 取消注册所有属于 [this] 的指令
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
*
* @return 这一时刻的浅拷贝.
*/
public fun CommandOwner.unregisterAllCommands()
public fun getRegisteredCommands(owner: CommandOwner): List<Command>
/**
* 取消注册所有属于 [owner] 的指令
*/
public fun unregisterAllCommands(owner: CommandOwner)
/**
* 注册一个指令.
@ -75,35 +73,31 @@ public interface CommandManager {
*
* 注意: [内建指令][BuiltInCommands] 也可以被覆盖.
*/
@JvmName("registerCommand")
public fun Command.register(override: Boolean = false): Boolean
public fun registerCommand(command: Command, override: Boolean = false): Boolean
/**
* 查找并返回重名的指令. 返回重名指令.
*/
@JvmName("findCommandDuplicate")
public fun Command.findDuplicate(): Command?
public fun findDuplicateCommand(command: Command): Command?
/**
* 取消注册这个指令.
*
* 若指令未注册, 返回 `false`.
*/
@JvmName("unregisterCommand")
public fun Command.unregister(): Boolean
public fun unregisterCommand(command: Command): Boolean
/**
* [this] 已经 [注册][register] 时返回 `true`
* [command] 已经 [注册][registerCommand] 时返回 `true`
*/
@JvmName("isCommandRegistered")
public fun Command.isRegistered(): Boolean
public fun isCommandRegistered(command: Command): Boolean
/**
* 解析并执行一个指令.
*
* 如要避免参数解析, 请使用 [Command.onCommand]
*
* ### 指令解析流程
* 1. [CommandCallParser] [MessageChain] 解析为 [CommandCall]
* 2. [CommandCallResolver] [CommandCall] 解析为 []
* 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理)
* 2. 参数语法分析.
* 在当前的实现下, [message] 被以空格和 [SingleMessage] 分割.
@ -112,7 +106,7 @@ public interface CommandManager {
* 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand]
*
* ### 扩展
* 参数语法分析过程可能会被扩展, 插件可以自定义处理方式, 因此可能不会简单地使用空格分隔.
* 参数语法分析过程可能会被扩展, 插件可以自定义处理方式 ([CommandCallParser]), 因此可能不会简单地使用空格分隔.
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
@ -120,42 +114,48 @@ public interface CommandManager {
* @see CommandCallParser
* @see CommandCallResolver
*
* @see CommandSender.executeCommand
* @see Command.execute
*
* @return 执行结果
*/
@ExperimentalCommandDescriptors
@JvmBlockingBridge
@OptIn(ExperimentalCommandDescriptors::class)
public suspend fun executeCommand(
caller: CommandSender,
message: Message,
checkPermission: Boolean = true,
): CommandExecuteResult {
return executeCommandImpl(this, message, caller, checkPermission)
return executeCommandImpl(message, caller, checkPermission)
}
/**
* 解析并执行一个指令
* 执行一个确切的指令
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
* @param command 目标指令
* @param arguments 参数列表
*
* @return 执行结果
* @see executeCommand
* @see executeCommand 获取更多信息
* @see Command.execute
*/
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true,
): CommandExecuteResult = executeCommand(this, PlainText(message).asMessageChain(), checkPermission)
@JvmName("resolveCall")
@ConsoleExperimentalApi
@JvmName("executeCommand")
@ExperimentalCommandDescriptors
public fun CommandCall.resolve(): ResolvedCommandCall? {
GlobalComponentStorage.run {
CommandCallResolverProvider.useExtensions { provider ->
provider.instance.resolve(this@resolve)?.let { return it }
}
@JvmSynthetic
public suspend fun executeCommand(
sender: CommandSender,
command: Command,
arguments: Message = EmptyMessageChain,
checkPermission: Boolean = true,
): CommandExecuteResult {
// TODO: 2020/10/18 net.mamoe.mirai.console.command.CommandManager.execute
val chain = buildMessageChain {
append(CommandManager.commandPrefix)
append(command.primaryName)
append(' ')
append(arguments)
}
return null
return CommandManager.executeCommand(sender, chain, checkPermission)
}
/**
@ -170,78 +170,89 @@ public interface CommandManager {
public fun matchCommand(commandName: String): Command?
public companion object INSTANCE : CommandManager by CommandManagerImpl {
// TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191
/**
* @see CommandManager.getRegisteredCommands
*/
@get:JvmName("registeredCommands0")
@get:JvmSynthetic
public inline val CommandOwner.registeredCommands: List<Command>
get() = getRegisteredCommands(this)
override val CommandOwner.registeredCommands: List<Command> get() = CommandManagerImpl.run { this@registeredCommands.registeredCommands }
override fun CommandOwner.unregisterAllCommands(): Unit = CommandManagerImpl.run { unregisterAllCommands() }
override fun Command.register(override: Boolean): Boolean = CommandManagerImpl.run { register(override) }
override fun Command.findDuplicate(): Command? = CommandManagerImpl.run { findDuplicate() }
override fun Command.unregister(): Boolean = CommandManagerImpl.run { unregister() }
override fun Command.isRegistered(): Boolean = CommandManagerImpl.run { isRegistered() }
override val commandPrefix: String get() = CommandManagerImpl.commandPrefix
override val allRegisteredCommands: List<Command>
get() = CommandManagerImpl.allRegisteredCommands
/**
* @see CommandManager.registerCommand
*/
@JvmSynthetic
public inline fun Command.register(override: Boolean = false): Boolean = registerCommand(this, override)
/**
* @see CommandManager.unregisterCommand
*/
@JvmSynthetic
public inline fun Command.unregister(): Boolean = unregisterCommand(this)
/**
* @see CommandManager.isCommandRegistered
*/
@get:JvmSynthetic
public inline val Command.isRegistered: Boolean
get() = isCommandRegistered(this)
/**
* @see CommandManager.unregisterAll
*/
@JvmSynthetic
public inline fun CommandOwner.unregisterAll(): Unit = unregisterAllCommands(this)
/**
* @see CommandManager.findDuplicate
*/
@JvmSynthetic
public inline fun Command.findDuplicate(): Command? = findDuplicateCommand(this)
}
}
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
* 解析并执行一个指令
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @return 执行结果
* @see executeCommand
*/
// @JvmBlockingBridge
// @JvmName("executeCommand")
public suspend fun Command.execute(
sender: CommandSender,
arguments: String = "",
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true,
): CommandExecuteResult = execute(sender, PlainText(arguments).asMessageChain(), checkPermission)
): CommandExecuteResult = CommandManager.executeCommand(this, PlainText(message).asMessageChain(), checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
// @JvmBlockingBridge
// @JvmName("executeCommand")
public suspend fun Command.execute(
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun Command.execute(
sender: CommandSender,
arguments: Message = EmptyMessageChain,
checkPermission: Boolean = true,
): CommandExecuteResult {
// TODO: 2020/10/18 net.mamoe.mirai.console.command.CommandManager.execute
val chain = buildMessageChain {
append(CommandManager.commandPrefix)
append(this@execute.primaryName)
append(' ')
append(arguments)
}
return CommandManager.executeCommand(sender, chain, checkPermission)
}
// Don't move into CommandManager, compilation error / VerifyError
@OptIn(ExperimentalCommandDescriptors::class)
internal suspend fun executeCommandImpl(
receiver: CommandManager,
message: Message,
caller: CommandSender,
checkPermission: Boolean,
): CommandExecuteResult = with(receiver) {
val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCall("")
val resolved = call.resolve() ?: return CommandExecuteResult.UnresolvedCall(call.calleeName)
val command = resolved.callee
if (checkPermission && !command.permission.testPermission(caller)) {
return CommandExecuteResult.PermissionDenied(command, call.calleeName)
}
return try {
resolved.calleeSignature.call(resolved)
CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain)
} catch (e: Throwable) {
CommandExecuteResult.ExecutionFailed(e, resolved.callee, call.calleeName, EmptyMessageChain)
}
}
): CommandExecuteResult = CommandManager.executeCommand(sender, this, arguments, checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun Command.execute(
sender: CommandSender,
arguments: String = "",
checkPermission: Boolean = true,
): CommandExecuteResult = execute(sender, PlainText(arguments), checkPermission)

View File

@ -12,6 +12,8 @@ package net.mamoe.mirai.console.command.resolve
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* The resolver converting a [CommandCall] into [ResolvedCommandCall] based on registered []
@ -22,4 +24,18 @@ import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
@ExperimentalCommandDescriptors
public interface CommandCallResolver {
public fun resolve(call: CommandCall): ResolvedCommandCall?
public companion object {
@JvmName("resolveCall")
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public fun CommandCall.resolve(): ResolvedCommandCall? {
GlobalComponentStorage.run {
CommandCallResolverProvider.useExtensions { provider ->
provider.instance.resolve(this@resolve)?.let { return it }
}
}
return null
}
}
}

View File

@ -15,12 +15,19 @@ import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.Command.Companion.allNames
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.findDuplicate
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall
import net.mamoe.mirai.console.command.resolve.CommandCallResolver.Companion.resolve
import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.message.MessageEvent
import net.mamoe.mirai.message.data.EmptyMessageChain
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.locks.ReentrantLock
@ -94,64 +101,90 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiCons
///// IMPL
override val CommandOwner.registeredCommands: List<Command> get() = _registeredCommands.filter { it.owner == this }
override fun getRegisteredCommands(owner: CommandOwner): List<Command> = _registeredCommands.filter { it.owner == owner }
override val allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy
override val commandPrefix: String get() = "/"
override fun CommandOwner.unregisterAllCommands() {
for (registeredCommand in registeredCommands) {
registeredCommand.unregister()
override fun unregisterAllCommands(owner: CommandOwner) {
for (registeredCommand in getRegisteredCommands(owner)) {
unregisterCommand(registeredCommand)
}
}
override fun Command.register(override: Boolean): Boolean {
if (this is CompositeCommand) {
this.overloads // init lazy
override fun registerCommand(command: Command, override: Boolean): Boolean {
if (command is CompositeCommand) {
command.overloads // init lazy
}
kotlin.runCatching {
this.permission // init lazy
this.secondaryNames // init lazy
this.description // init lazy
this.usage // init lazy
command.permission // init lazy
command.secondaryNames // init lazy
command.description // init lazy
command.usage // init lazy
}.onFailure {
throw IllegalStateException("Failed to init command ${this@register}.", it)
throw IllegalStateException("Failed to init command ${command}.", it)
}
modifyLock.withLock {
this@CommandManagerImpl.modifyLock.withLock {
if (!override) {
if (findDuplicate() != null) return false
if (command.findDuplicate() != null) return false
}
_registeredCommands.add(this@register)
if (this.prefixOptional) {
for (name in this.allNames) {
this@CommandManagerImpl._registeredCommands.add(command)
if (command.prefixOptional) {
for (name in command.allNames) {
val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap[lowerCaseName] = this
requiredPrefixCommandMap[lowerCaseName] = this
this@CommandManagerImpl.optionalPrefixCommandMap[lowerCaseName] = command
this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
}
} else {
for (name in this.allNames) {
for (name in command.allNames) {
val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
requiredPrefixCommandMap[lowerCaseName] = this
this@CommandManagerImpl.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
}
}
return true
}
}
override fun Command.findDuplicate(): Command? =
_registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames }
override fun findDuplicateCommand(command: Command): Command? =
_registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase command.allNames }
override fun Command.unregister(): Boolean = modifyLock.withLock {
if (this.prefixOptional) {
this.allNames.forEach {
override fun unregisterCommand(command: Command): Boolean = modifyLock.withLock {
if (command.prefixOptional) {
command.allNames.forEach {
optionalPrefixCommandMap.remove(it.toLowerCase())
}
}
this.allNames.forEach {
command.allNames.forEach {
requiredPrefixCommandMap.remove(it.toLowerCase())
}
_registeredCommands.remove(this)
_registeredCommands.remove(command)
}
override fun Command.isRegistered(): Boolean = this in _registeredCommands
}
override fun isCommandRegistered(command: Command): Boolean = command in _registeredCommands
}
// Don't move into CommandManager, compilation error / VerifyError
@OptIn(ExperimentalCommandDescriptors::class)
internal suspend fun executeCommandImpl(
message: Message,
caller: CommandSender,
checkPermission: Boolean,
): CommandExecuteResult {
val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCall("")
val resolved = call.resolve() ?: return CommandExecuteResult.UnresolvedCall(call.calleeName)
val command = resolved.callee
if (checkPermission && !command.permission.testPermission(caller)) {
return CommandExecuteResult.PermissionDenied(command, call.calleeName)
}
return try {
resolved.calleeSignature.call(resolved)
CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain)
} catch (e: Throwable) {
CommandExecuteResult.ExecutionFailed(e, resolved.callee, call.calleeName, EmptyMessageChain)
}
}

View File

@ -16,11 +16,11 @@ import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.Testing
import net.mamoe.mirai.console.Testing.withTesting
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.getRegisteredCommands
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registeredCommands
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.registerCommand
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterCommand
import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext
@ -78,20 +78,20 @@ internal class TestCommand {
@Test
fun testRegister() {
try {
ConsoleCommandOwner.unregisterAllCommands() // builtins
TestSimpleCommand.unregister()
unregisterAllCommands(ConsoleCommandOwner) // builtins
unregisterCommand(TestSimpleCommand)
assertTrue(TestCompositeCommand.register())
assertFalse(TestCompositeCommand.register())
assertEquals(1, ConsoleCommandOwner.registeredCommands.size)
assertEquals(1, getRegisteredCommands(ConsoleCommandOwner).size)
assertEquals(1, CommandManagerImpl._registeredCommands.size)
assertEquals(2,
CommandManagerImpl.requiredPrefixCommandMap.size,
CommandManagerImpl.requiredPrefixCommandMap.entries.joinToString { it.toString() })
} finally {
TestCompositeCommand.unregister()
unregisterCommand(TestCompositeCommand)
}
}
@ -194,7 +194,7 @@ internal class TestCommand {
}
}
composite.register()
registerCommand(composite)
println(composite.overloads.joinToString())

View File

@ -10,13 +10,13 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterCommand
inline fun <T : Command, R> T.withRegistration(block: T.() -> R): R {
this.register()
try {
return block()
} finally {
this.unregister()
unregisterCommand(this)
}
}

View File

@ -15,11 +15,7 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.CommandExecuteStatus
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.terminal.noconsole.NoConsole
import net.mamoe.mirai.console.util.ConsoleInternalApi