API stabilization: Simplify command execution in PluginManager

This commit is contained in:
Him188 2020-08-29 23:44:49 +08:00
parent e9595075e0
commit 27952f537f
5 changed files with 142 additions and 98 deletions

View File

@ -9,7 +9,7 @@
@file:Suppress(
"NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE",
"MemberVisibilityCanBePrivate"
"MemberVisibilityCanBePrivate", "INAPPLICABLE_JVM_NAME"
)
@file:JvmName("CommandManagerKt")
@ -17,10 +17,8 @@ package net.mamoe.mirai.console.command
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand
import net.mamoe.mirai.message.data.*
/**
* 指令管理器
@ -63,74 +61,92 @@ public interface CommandManager {
*
* 注意: [内建指令][BuiltInCommands] 也可以被覆盖.
*/
@JvmName("registerCommand")
public fun Command.register(override: Boolean = false): Boolean
/**
* 查找并返回重名的指令. 返回重名指令.
*/
@JvmName("findCommandDuplicate")
public fun Command.findDuplicate(): Command?
/**
* 取消注册这个指令. 若指令未注册, 返回 `false`.
*/
@JvmName("unregisterCommand")
public fun Command.unregister(): Boolean
/**
* [this] 已经 [注册][register] 后返回 `true`
*/
@JvmName("isCommandRegistered")
public fun Command.isRegistered(): Boolean
/**
* 执行一个指令
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
* @see executeCommand
*/
@JvmBlockingBridge
@Throws(CommandExecutionException::class)
public suspend fun Command.execute(sender: CommandSender, args: MessageChain, checkPermission: Boolean = true)
/**
* 执行一个指令
*
* @return 成功执行的指令, 在无匹配指令时返回 `null`
* @throws CommandExecutionException [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出
* @see executeCommand
*/
@JvmBlockingBridge
@Throws(CommandExecutionException::class)
public suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean = true)
/**
* 解析并执行一个指令, 获取详细的指令参数等信息
*
* 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionFailed]
* 解析并执行一个指令
*
* ### 指令解析流程
* 1. [messages] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理)
* 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理)
* 2. 参数语法分析.
* 在当前的实现下, [messages] 被以空格和 [SingleMessage] 分割.
* 在当前的实现下, [message] 被以空格和 [SingleMessage] 分割.
* "MessageChain("foo bar", [Image], " test")" 被分割为 "foo", "bar", [Image], "test".
* 注意: 字符串与消息元素之间不需要空格, 会被强制分割. "bar[mirai:image:]" 会被分割为 "bar" [Image] 类型的消息元素.
* 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand]
*
* @param messages 接受 [String] [Message], 其他对象将会被 [Any.toString]
* ### 未来的扩展
* 在将来, 参数语法分析过程可能会被扩展, 允许插件自定义处理方式, 因此可能不会简单地使用空格分隔.
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @return 执行结果
*/
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(vararg messages: Any): CommandExecuteResult
public suspend fun CommandSender.executeCommand(
message: Message,
checkPermission: Boolean = true
): CommandExecuteResult
/**
* 解析并执行一个指令, 获取详细的指令参数等信息
* 解析并执行一个指令
*
* 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionFailed]
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @return 执行结果
* @see executeCommand
*/
@JvmDefault
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true
): CommandExecuteResult = executeCommand(PlainText(message).asMessageChain(), checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(messages: MessageChain): CommandExecuteResult
@JvmName("executeCommand")
public suspend fun Command.execute(
sender: CommandSender,
arguments: Message = EmptyMessageChain,
checkPermission: Boolean = true
): CommandExecuteResult
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmDefault
@JvmBlockingBridge
@JvmName("executeCommand")
public suspend fun Command.execute(
sender: CommandSender,
arguments: String = "",
checkPermission: Boolean = true
): CommandExecuteResult = execute(sender, PlainText(arguments).asMessageChain(), checkPermission)
public companion object INSTANCE : CommandManager by CommandManagerImpl {
// TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191
@ -142,19 +158,31 @@ public interface CommandManager {
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
override suspend fun Command.execute(
sender: CommandSender,
args: MessageChain,
arguments: Message,
checkPermission: Boolean
): Unit = CommandManagerImpl.run { execute(sender, args = args, checkPermission = checkPermission) }
): CommandExecuteResult =
CommandManagerImpl.run { execute(sender, arguments = arguments, checkPermission = checkPermission) }
override suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean): Unit =
CommandManagerImpl.run { execute(sender, args = args, checkPermission = checkPermission) }
override suspend fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean
): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) }
override suspend fun CommandSender.executeCommand(vararg messages: Any): CommandExecuteResult =
CommandManagerImpl.run { executeCommand(*messages) }
override suspend fun Command.execute(
sender: CommandSender,
arguments: String,
checkPermission: Boolean
): CommandExecuteResult = CommandManagerImpl.run { execute(sender, arguments, checkPermission) }
override suspend fun CommandSender.executeCommand(messages: MessageChain): CommandExecuteResult =
CommandManagerImpl.run { executeCommand(messages) }
override suspend fun CommandSender.executeCommand(
message: Message,
checkPermission: Boolean
): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) }
}
}

View File

@ -121,6 +121,7 @@ public interface GroupAwareCommandSender : CommandSender {
/**
* 控制台指令执行者. 代表由控制台执行指令
* @see INSTANCE
*/
// 前端实现
public abstract class ConsoleCommandSender internal constructor() : CommandSender {

View File

@ -18,8 +18,10 @@ import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.message.MessageEvent
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageContent
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
@ -137,34 +139,43 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
override fun Command.isRegistered(): Boolean = this in registeredCommands
//// executing without detailed result (faster)
override suspend fun Command.execute(sender: CommandSender, args: MessageChain, checkPermission: Boolean) {
sender.executeCommandInternal(
override suspend fun Command.execute(
sender: CommandSender,
arguments: Message,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
args.flattenCommandComponents().toTypedArray(),
this.primaryName,
arguments.flattenCommandComponents().toTypedArray(),
primaryName,
checkPermission
)
}
override suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean) {
sender.executeCommandInternal(
override suspend fun Command.execute(
sender: CommandSender,
arguments: String,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
args.flattenCommandComponents().toTypedArray(),
this.primaryName,
arguments.flattenCommandComponents().toTypedArray(),
primaryName,
checkPermission
)
}
//// execution with detailed result
override suspend fun CommandSender.executeCommand(vararg messages: Any): CommandExecuteResult {
if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(messages, messages[0].toString().substringBefore(' '))
}
override suspend fun CommandSender.executeCommand(messages: MessageChain): CommandExecuteResult {
val msg = messages.filterIsInstance<MessageContent>()
override suspend fun CommandSender.executeCommand(
message: Message,
checkPermission: Boolean
): CommandExecuteResult {
val msg = message.asMessageChain().filterIsInstance<MessageContent>()
if (msg.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(msg, msg[0].toString().substringBefore(' '))
return executeCommandInternal(msg, msg[0].content.substringBefore(' '), checkPermission)
}
override suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean): CommandExecuteResult {
if (message.isBlank()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(message, message.substringBefore(' '), checkPermission)
}
}

View File

@ -10,7 +10,6 @@
package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.description.CommandArgumentParserException
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import kotlin.math.max
@ -148,34 +147,11 @@ internal suspend fun CommandSender.executeCommandInternal(
args: Array<out Any>,
commandName: String,
checkPermission: Boolean
) {
if (checkPermission && !command.testPermission(this)) {
throw CommandExecutionException(this, command, commandName, CommandPermissionDeniedException(this, command))
}
kotlin.runCatching {
command.onCommand(this, args)
}.onFailure {
catchExecutionException(it)
if (it !is CommandArgumentParserException) {
throw CommandExecutionException(this, command, commandName, it)
}
}
}
@JvmSynthetic
internal suspend fun CommandSender.executeCommandInternal(
messages: Any,
commandName: String
): CommandExecuteResult {
val command =
CommandManagerImpl.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName)
val args = messages.flattenCommandComponents().dropToTypedArray(1)
if (!command.testPermission(this)) {
if (checkPermission && !command.testPermission(this)) {
return CommandExecuteResult.PermissionDenied(command, commandName)
}
kotlin.runCatching {
command.onCommand(this, args)
}.fold(
@ -196,3 +172,17 @@ internal suspend fun CommandSender.executeCommandInternal(
}
)
}
@JvmSynthetic
internal suspend fun CommandSender.executeCommandInternal(
messages: Any,
commandName: String,
checkPermission: Boolean
): CommandExecuteResult {
val command =
CommandManagerImpl.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName)
val args = messages.flattenCommandComponents().dropToTypedArray(1)
return executeCommandInternal(command, args, commandName, checkPermission)
}

View File

@ -30,6 +30,7 @@ import net.mamoe.mirai.console.internal.command.flattenCommandComponents
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.buildMessageChain
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
@ -91,7 +92,7 @@ internal class TestCommand {
@Test
fun testSimpleExecute() = runBlocking {
assertEquals(arrayOf("test").contentToString(), withTesting<Array<String>> {
TestSimpleCommand.execute(sender, "test")
assertSuccess(TestSimpleCommand.execute(sender, "test"))
}.contentToString())
}
@ -108,7 +109,7 @@ internal class TestCommand {
@Test
fun testSimpleArgsSplitting() = runBlocking {
assertEquals(arrayOf("test", "ttt", "tt").contentToString(), withTesting<Array<String>> {
TestSimpleCommand.execute(sender, PlainText("test ttt tt"))
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt")))
}.contentToString())
}
@ -117,16 +118,20 @@ internal class TestCommand {
@Test
fun `PlainText and Image args splitting`() = runBlocking {
val result = withTesting<Array<Any>> {
TestSimpleCommand.execute(sender, "test", image, "tt")
assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain {
+"test"
+image
+"tt"
}))
}
assertEquals(arrayOf("test", image, "tt").contentToString(), result.contentToString())
assertSame(image, result[1])
}
@Test
fun `test throw Exception`() = assertTrue {
fun `test throw Exception`() {
runBlocking {
sender.executeCommand("").isFailure()
assertTrue(sender.executeCommand("").isFailure())
}
}
@ -134,7 +139,7 @@ internal class TestCommand {
fun `executing command by string command`() = runBlocking {
TestCompositeCommand.register()
val result = withTesting<Int> {
assertNotNull(sender.executeCommand("/testComposite", "mute 1"))
assertSuccess(sender.executeCommand("/testComposite mute 1"))
}
assertEquals(1, result)
@ -143,7 +148,7 @@ internal class TestCommand {
@Test
fun `composite command executing`() = runBlocking {
assertEquals(1, withTesting {
assertNotNull(TestCompositeCommand.execute(sender, "mute 1"))
assertSuccess(TestCompositeCommand.execute(sender, "mute 1"))
})
}
@ -209,7 +214,12 @@ internal class TestCommand {
composite.withRegistration {
assertEquals(333, withTesting<MyClass> { execute(sender, "mute 333") }.value)
assertEquals(2, withTesting<MyClass> { execute(sender, "mute", image) }.value)
assertEquals(2, withTesting<MyClass> {
execute(sender, buildMessageChain {
+"mute"
+image
})
}.value)
}
}
}
@ -232,3 +242,7 @@ internal class TestCommand {
}
}
}
internal fun assertSuccess(result: CommandExecuteResult) {
assertTrue(result.isSuccess(), result.toString())
}