From 775b888273175521003f1dd66c95edff8a2a29e9 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 23 Aug 2020 02:25:42 +0800 Subject: [PATCH] Documentations and several improvements --- .../mirai/console/command/BuiltInCommands.kt | 5 +- .../mamoe/mirai/console/command/Command.kt | 26 ++-- .../command/CommandExecutionException.kt | 4 + .../mirai/console/command/CommandManager.kt | 18 +++ .../CommandPermissionDeniedException.kt | 4 + .../mirai/console/command/CommandSender.kt | 36 +++++- .../mirai/console/command/CompositeCommand.kt | 116 +++++++++++++++++- .../mamoe/mirai/console/command/RawCommand.kt | 100 +++++++++++++++ .../mirai/console/command/SimpleCommand.kt | 61 ++++++--- .../description/CommandArgParserBuiltins.kt | 6 +- .../description/CommandArgumentContext.kt | 105 +++++++++++----- .../description/CommandArgumentParser.kt | 78 +++++++++++- .../command/CommandManagerImpl.kt | 8 +- .../command}/CompositeCommand.CommandParam.kt | 3 +- .../command/CompositeCommandInternal.kt | 1 - .../console/internal/command/internal.kt | 10 +- .../mirai/console/command/TestCommand.kt | 4 +- 17 files changed, 506 insertions(+), 79 deletions(-) rename backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/{ => internal}/command/CommandManagerImpl.kt (94%) rename backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/{command/description => internal/command}/CompositeCommand.CommandParam.kt (93%) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt index a06b1725a..0817a9677 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt @@ -16,8 +16,9 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.CommandManagerImpl.allRegisteredCommands -import net.mamoe.mirai.console.command.CommandManagerImpl.register +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register +import net.mamoe.mirai.console.internal.command.CommandManagerImpl +import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.console.util.ConsoleInternalAPI import net.mamoe.mirai.message.nextMessageOrNull 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 f22297850..24954bfb4 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 @@ -12,7 +12,7 @@ package net.mamoe.mirai.console.command import net.mamoe.kjbb.JvmBlockingBridge -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.internal.command.isValidSubName import net.mamoe.mirai.message.data.SingleMessage @@ -53,14 +53,17 @@ public interface Command { public val prefixOptional: Boolean /** - * 指令拥有者, 对于插件的指令通常是 [PluginCommandOwner] + * 指令拥有者. + * @see CommandOwner */ public val owner: CommandOwner /** + * 在指令被执行时调用. + * * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. * - * @see CommandManager.execute + * @see CommandManager.executeCommand 查看更多信息 */ @JvmBlockingBridge public suspend fun CommandSender.onCommand(args: Array) @@ -81,16 +84,23 @@ public suspend inline fun Command.onCommand(sender: CommandSender, args: Array = + public override val description: String = description.trimIndent() + public override val names: Array = names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") } }.toTypedArray() diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt index 2feace762..2f7b98967 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt @@ -18,6 +18,10 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand * 在 [CommandManager.executeCommand] 中, [Command.onCommand] 抛出异常时包装的异常. */ public class CommandExecutionException( + /** + * 执行者 + */ + public val sender: CommandSender, /** * 执行过程发生异常的指令 */ 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 6c349fcd6..75fae05f1 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 @@ -16,9 +16,15 @@ 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 +/** + * 指令管理器 + */ public interface CommandManager { /** * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. @@ -79,6 +85,7 @@ public interface CommandManager { * * @return 成功执行的指令, 在无匹配指令时返回 `null` * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * @see executeCommand */ @JvmBlockingBridge @Throws(CommandExecutionException::class) @@ -89,6 +96,7 @@ public interface CommandManager { * * @return 成功执行的指令, 在无匹配指令时返回 `null` * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * @see executeCommand */ @JvmBlockingBridge @Throws(CommandExecutionException::class) @@ -97,6 +105,16 @@ public interface CommandManager { /** * 解析并执行一个指令, 获取详细的指令参数等信息 * + * 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionFailed] + * + * ### 指令解析流程 + * 1. [messages] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理) + * 2. 参数语法分析. + * 在当前的实现下, [messages] 被以空格和 [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] * * @return 执行结果 diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt index 512b90900..63e355266 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt @@ -18,6 +18,10 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand * 总是作为 [CommandExecutionException.cause]. */ public class CommandPermissionDeniedException( + /** + * 执行者 + */ + public val commandSender: CommandSender, /** * 执行过程发生异常的指令 */ 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 111d9edfe..718a52c61 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 @@ -24,10 +24,34 @@ import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.PlainText /** - * 指令发送者 + * 指令发送者. + * + * ### 获得指令发送者 + * - [MessageEvent.toCommandSender] + * - [FriendMessageEvent.toCommandSender] + * - [GroupMessageEvent.toCommandSender] + * - [TempMessageEvent.toCommandSender] + * + * - [Member.asCommandSender] + * - [Friend.asCommandSender] + * - [User.asCommandSender] + * + * ### 子类型 + * + * 当真实收到由用户执行的指令时: + * - 若用户在群内指令执行, 对应 [CommandSender] 为 [MemberCommandSenderOnMessage] + * - 若用户在私聊环境内指令执行, 对应 [CommandSender] 为 [FriendCommandSenderOnMessage] + * - 若用户在临时会话内指令执行, 对应 [CommandSender] 为 [TempCommandSenderOnMessage] + * + * 当指令由其他插件主动执行时, 插件应使用 [toCommandSender] 或 [asCommandSender], 因此 + * - 若用户在群内指令执行, 对应 [CommandSender] 为 [MemberCommandSender] + * - 若用户在私聊环境内指令执行, 对应 [CommandSender] 为 [FriendCommandSender] + * - 若用户在临时会话内指令执行, 对应 [CommandSender] 为 [TempCommandSender] * * @see ConsoleCommandSender 控制台 * @see UserCommandSender [User] ([群成员][Member], [好友][Friend]) + * @see toCommandSender + * @see asCommandSender */ @Suppress("FunctionName") public interface CommandSender { @@ -109,10 +133,16 @@ public abstract class ConsoleCommandSender internal constructor() : CommandSende } } +@ConsoleExperimentalAPI public fun FriendMessageEvent.toCommandSender(): FriendCommandSenderOnMessage = FriendCommandSenderOnMessage(this) + +@ConsoleExperimentalAPI public fun GroupMessageEvent.toCommandSender(): MemberCommandSenderOnMessage = MemberCommandSenderOnMessage(this) + +@ConsoleExperimentalAPI public fun TempMessageEvent.toCommandSender(): TempCommandSenderOnMessage = TempCommandSenderOnMessage(this) +@ConsoleExperimentalAPI public fun MessageEvent.toCommandSender(): CommandSenderOnMessage<*> = when (this) { is FriendMessageEvent -> toCommandSender() is GroupMessageEvent -> toCommandSender() @@ -120,9 +150,13 @@ public fun MessageEvent.toCommandSender(): CommandSenderOnMessage<*> = when (thi else -> throw IllegalArgumentException("unsupported MessageEvent: ${this::class.qualifiedNameOrTip}") } +@ConsoleExperimentalAPI public fun Member.asCommandSender(): MemberCommandSender = MemberCommandSender(this) + +@ConsoleExperimentalAPI public fun Friend.asCommandSender(): FriendCommandSender = FriendCommandSender(this) +@ConsoleExperimentalAPI public fun User.asCommandSender(): UserCommandSender { return when (this) { is Friend -> this.asCommandSender() diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt index b1e3df066..db8a3d28d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -26,7 +26,51 @@ import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.reflect.KClass /** - * 复合指令. + * 复合指令. 指令注册时候会通过反射构造指令解析器. + * + * 示例: + * ``` + * @OptIn(ConsoleExperimentalAPI::class) + * object MyCompositeCommand : CompositeCommand( + * MyPluginMain, "manage", // "manage" 是主指令名 + * description = "示例指令", permission = MyCustomPermission, + * // prefixOptional = true // 还有更多参数可填, 此处忽略 + * ) { + * + * // [参数智能解析] + * // + * // 在控制台执行 "/manage <群号>.<群员> <持续时间>", + * // 或在聊天群内发送 "/manage <@一个群员> <持续时间>", + * // 或在聊天群内发送 "/manage <目标群员的群名> <持续时间>", + * // 或在聊天群内发送 "/manage <目标群员的账号> <持续时间>" + * // 时调用这个函数 + * @SubCommand + * suspend fun CommandSender.mute(target: Member, duration: Int) { // 通过 /manage mute 调用 + * sendMessage("/manage mute 被调用了, 参数为: $target, $duration") + * + * val result = kotlin.runCatching { + * target.mute(duration).toString() + * }.getOrElse { + * it.stackTraceToString() + * } // 失败时返回堆栈信息 + * + * sendMessage("结果: $result") + * } + * + * @SubCommand + * suspend fun CommandSender.list() { // 执行 "/manage list" 时调用这个函数 + * sendMessage("/manage list 被调用了") + * } + * + * // 支持 Image 类型, 需在聊天中执行此指令. + * @SubCommand + * suspend fun CommandSender.test(image: Image) { // 执行 "/manage test <一张图片>" 时调用这个函数 + * sendMessage("/manage image 被调用了, 图片是 ${image.imageId}") + * } + * } + * ``` + * + * @see buildCommandArgumentContext */ @ConsoleExperimentalAPI public abstract class CompositeCommand @JvmOverloads constructor( @@ -80,3 +124,73 @@ public abstract class CompositeCommand @JvmOverloads constructor( internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver get() = CompositeCommandSubCommandAnnotationResolver } + + +/** + * 复合指令. 指令注册时候会通过反射构造指令解析器. + * + * 示例: + * ``` + * public final class MyCompositeCommand extends CompositeCommand { + * public static final MyCompositeCommand INSTANCE = new MyCompositeCommand(); + * + * public MyCompositeCommand() { + * super(MyPluginMain.INSTANCE, "manage") // "manage" 是主指令名 + * } + * + * // [参数智能解析] + * // + * // + * // 在控制台执行 "/manage <群号>.<群员> <持续时间>", + * // 或在聊天群内发送 "/manage <@一个群员> <持续时间>", + * // 或在聊天群内发送 "/manage <目标群员的群名> <持续时间>", + * // 或在聊天群内发送 "/manage <目标群员的账号> <持续时间>" + * // 时调用这个函数 + * @SubCommand + * public void mute(CommandSender sender, Member target, int duration) { // 通过 /manage mute 调用. + * sender.sendMessage("/manage mute 被调用了, 参数为: " + target + ", " + duration); + * + * + * String result; + * try { + * result = target.mute(duration).toString(); + * } catch(Exception e) { + * result = ExceptionsKt.stackTraceToString(e); + * } + * + * sender.sendMessage("结果: " + result) + * } + * + * @SubCommand + * public void list(CommandSender sender) { // 执行 "/manage list" 时调用这个方法 + * sender.sendMessage("/manage list 被调用了") + * } + * + * // 支持 Image 类型, 需在聊天中执行此指令. + * @SubCommand + * public void test(CommandSender sender, Image image) { // 执行 "/manage test <一张图片>" 时调用这个方法 + * sender.sendMessage("/manage image 被调用了, 图片是 " + image.imageId) + * } + * } + * ``` + * + * @see buildCommandArgumentContext + */ +@ConsoleExperimentalAPI +public abstract class JCompositeCommand( + owner: CommandOwner, + vararg names: String +) : CompositeCommand(owner, *names) { + /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ + public final override var description: String = "" + protected set + + /** 指令权限 */ + public final override var permission: CommandPermission = CommandPermission.Default + protected set + + /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + public final override var prefixOptional: Boolean = false + protected set + +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt index 14590c53e..890d13079 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -7,18 +7,118 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("unused") + package net.mamoe.mirai.console.command +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand +import net.mamoe.mirai.message.data.SingleMessage + /** * 无参数解析, 接收原生参数的指令. + * + * ### 指令执行流程 + * 继 [CommandManager.executeCommand] 所述第 3 步, [RawCommand] 不会对参数做任何解析. + * + * @see JRawCommand 供 Java 用户继承. + * + * @see SimpleCommand 简单指令 + * @see CompositeCommand 复合指令 */ public abstract class RawCommand @JvmOverloads constructor( + /** + * 指令拥有者. + * @see CommandOwner + */ public override val owner: CommandOwner, + /** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ public override vararg val names: String, + /** 用法说明, 用于发送给用户 */ public override val usage: String = "", + /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ public override val description: String = "", + /** 指令权限 */ public override val permission: CommandPermission = CommandPermission.Default, + /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ public override val prefixOptional: Boolean = false ) : Command { + /** + * 在指令被执行时调用. + * + * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + * + * @see CommandManager.execute 查看更多信息 + */ public abstract override suspend fun CommandSender.onCommand(args: Array) } + + +/** + * 供 Java 用户继承 + * + * 请在构造时设置相关属性. + * + * ```java + * public final class MyCommand extends JRawCommand { + * public static final MyCommand INSTANCE = new MyCommand(); + * private MyCommand () { + * super(MyPluginMain.INSTANCE, "test") + * // 可选设置如下属性 + * setUsage("/test") + * setDescription("这是一个测试指令") + * setPermission(CommandPermission.Operator.INSTANCE) + * setPrefixOptional(true) + * } + * + * @Override + * public void onCommand(@NotNull CommandSender sender, @NotNull args: Object[]) { + * // 处理指令 + * } + * } + * ``` + * + * @see RawCommand + */ +public abstract class JRawCommand( + /** + * 指令拥有者. + * @see CommandOwner + */ + public override val owner: CommandOwner, + /** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ + public override vararg val names: String +) : Command { + /** 用法说明, 用于发送给用户 */ + public override var usage: String = "" + protected set + + /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ + public final override var description: String = "" + protected set + + /** 指令权限 */ + public final override var permission: CommandPermission = CommandPermission.Default + protected set + + /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ + public final override var prefixOptional: Boolean = false + protected set + + /** + * 在指令被执行时调用. + * + * @param args 指令参数. 数组元素类型可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + * + * @see CommandManager.execute 查看更多信息 + */ + @Suppress("INAPPLICABLE_JVM_NAME") + @JvmName("onCommand") + public abstract fun onCommand(sender: CommandSender, args: Array) + + public final override suspend fun CommandSender.onCommand(args: Array) { + withContext(Dispatchers.IO) { onCommand(this@onCommand, args) } + } +} \ No newline at end of file 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 index 55900a418..d4e2f1174 100644 --- 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 @@ -17,19 +17,23 @@ package net.mamoe.mirai.console.command +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver /** - * 简单指令. 参数支持自动解析. [CommandArgumentParser] + * 简单的, 支持参数自动解析的指令. + * + * 要查看指令解析流程, 参考 [CommandManager.executeCommand] + * 要查看参数解析方式, 参考 [CommandArgumentParser] * * Kotlin 实现: * ``` * object MySimpleCommand : SimpleCommand( * MyPlugin, "tell", * description = "Message somebody", - * usage = "/tell " + * usage = "/tell " // usage 如不设置则自动根据带有 @Handler 的方法生成 * ) { * @Handler * suspend fun CommandSender.onCommand(target: User, message: String) { @@ -38,18 +42,8 @@ import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotatio * } * ``` * - * Java 实现: - * ```java - * public final class MySimpleCommand extends SimpleCommand { - * private MySimpleCommand() { - * super(MyPlugin.INSTANCE, new String[]{ "tell" }, "Message somebody", "/tell ") - * } - * @Handler - * public void onCommand(CommandSender sender, User target, String message) { - * target.sendMessage(message) - * } - * } - * ``` + * @see JSimpleCommand Java 实现 + * @see [CommandManager.executeCommand] */ public abstract class SimpleCommand @JvmOverloads constructor( owner: CommandOwner, @@ -69,7 +63,7 @@ public abstract class SimpleCommand @JvmOverloads constructor( */ protected annotation class Handler - public final override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext + public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext public final override suspend fun CommandSender.onCommand(args: Array) { subCommands.single().parseAndExecute(this, args, false) @@ -85,4 +79,41 @@ public abstract class SimpleCommand @JvmOverloads constructor( internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver get() = SimpleCommandSubCommandAnnotationResolver +} + +/** + * Java 实现: + * ```java + * public final class MySimpleCommand extends JSimpleCommand { + * private MySimpleCommand() { + * super(MyPlugin.INSTANCE, "tell") + * // 可选设置如下属性 + * setDescription("这是一个测试指令") + * setUsage("/tell ") // 如不设置则自动根据带有 @Handler 的方法生成 + * setPermission(CommandPermission.Operator.INSTANCE) + * setPrefixOptional(true) + * } + * + * @Handler + * public void onCommand(CommandSender sender, User target, String message) { + * target.sendMessage(message) + * } + * } + * ``` + * + * @see SimpleCommand + * @see [CommandManager.executeCommand] + */ +public abstract class JSimpleCommand( + owner: CommandOwner, + vararg names: String +) : SimpleCommand(owner, *names) { + public override var description: String = super.description + protected set + public override var permission: CommandPermission = super.permission + protected set + public override var prefixOptional: Boolean = super.prefixOptional + protected set + public override var context: CommandArgumentContext = super.context + protected set } \ No newline at end of file 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 90822791c..4b3c7f4f9 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 @@ -92,7 +92,7 @@ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions { +public object ExistingBotArgumentParser : InternalCommandArgumentParserExtensions { public override fun parse(raw: String, sender: CommandSender): Bot = if (raw == "~") sender.inferBotOrFail() else raw.findBotOrFail() @@ -108,7 +108,7 @@ public object ExistBotArgumentParser : InternalCommandArgumentParserExtensions { +public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions { private val syntax = """ - `botId.friendId` - `botId.friendNick` (模糊搜索, 寻找最优匹配) @@ -153,7 +153,7 @@ public object ExistFriendArgumentParser : InternalCommandArgumentParserExtension * * 当只登录了一个 [Bot] 时, 无需上述 `botId` 参数即可 */ -public object ExistGroupArgumentParser : InternalCommandArgumentParserExtensions { +public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions { private val syntax = """ - `botId.groupId` - `~` (指代指令调用人自己所在群. 仅群聊天环境下) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt index 001132087..15ad2ca1f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt @@ -26,15 +26,18 @@ import kotlin.reflect.full.isSubclassOf /** - * [CommandArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. + * 指令参数环境, 即 [CommandArgumentParser] 的集合, 用于 [CompositeCommand] 和 [SimpleCommand]. + * + * 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器 + * + * 要构造 [CommandArgumentContext], 参考 [buildCommandArgumentContext] * * @see SimpleCommandArgumentContext 简单实现 * @see EmptyCommandArgumentContext 空实现, 类似 [emptyList] - * @see CommandArgumentContext.EMPTY 空实现的另一种获取方式. * * @see CommandArgumentContext.Builtins 内建 [CommandArgumentParser] * - * @see CommandArgumentContext DSL + * @see buildCommandArgumentContext DSL 构造 */ public interface CommandArgumentContext { /** @@ -62,7 +65,7 @@ public interface CommandArgumentContext { /** * 内建的默认 [CommandArgumentParser] */ - public object Builtins : CommandArgumentContext by (CommandArgumentContext { + public object Builtins : CommandArgumentContext by (buildCommandArgumentContext { Int::class with IntArgumentParser Byte::class with ByteArgumentParser Short::class with ShortArgumentParser @@ -73,14 +76,14 @@ public interface CommandArgumentContext { Float::class with FloatArgumentParser Member::class with ExistMemberArgumentParser - Group::class with ExistGroupArgumentParser - Friend::class with ExistFriendArgumentParser - Bot::class with ExistBotArgumentParser + Group::class with ExistingGroupArgumentParser + Friend::class with ExistingFriendArgumentParser + Bot::class with ExistingBotArgumentParser }) } /** - * 拥有 [CommandArgumentContext] 的类 + * 拥有 [buildCommandArgumentContext] 的类 * * @see SimpleCommand * @see CompositeCommand @@ -95,7 +98,7 @@ public interface CommandArgumentContextAware { public object EmptyCommandArgumentContext : CommandArgumentContext by SimpleCommandArgumentContext(listOf()) /** - * 合并两个 [CommandArgumentContext], [replacer] 将会替换 [this] 中重复的 parser. + * 合并两个 [buildCommandArgumentContext], [replacer] 将会替换 [this] 中重复的 parser. */ public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext): CommandArgumentContext { if (replacer == EmptyCommandArgumentContext) return this @@ -125,9 +128,9 @@ public operator fun CommandArgumentContext.plus(replacer: List>): } /** - * 自定义 [CommandArgumentContext] + * 自定义 [buildCommandArgumentContext] * - * @see CommandArgumentContext + * @see buildCommandArgumentContext */ @Suppress("UNCHECKED_CAST") public class SimpleCommandArgumentContext( @@ -140,10 +143,11 @@ public class SimpleCommandArgumentContext( } /** - * 构建一个 [CommandArgumentContext]. + * 构建一个 [buildCommandArgumentContext]. * + * Kotlin 实现: * ``` - * CommandArgumentContext { + * val context = buildCommandArgumentContext { * Int::class with IntArgParser * Member::class with ExistMemberArgParser * Group::class with { s: String, sender: CommandSender -> @@ -155,22 +159,47 @@ public class SimpleCommandArgumentContext( * } * ``` * + * Java 实现: + * ```java + * CommandArgumentContext context = + * new CommandArgumentContextBuilder() + * .add(clazz1, parser1) + * .add(String.class, new CommandArgumentParser() { + * public String parse(String raw, CommandSender sender) { + * // ... + * } + * }) + * // 更多 add + * .build() + * ``` + * * @see CommandArgumentContextBuilder - * @see CommandArgumentContext + * @see buildCommandArgumentContext */ -@Suppress("FunctionName") @JvmSynthetic -public fun CommandArgumentContext(block: CommandArgumentContextBuilder.() -> Unit): CommandArgumentContext { - return SimpleCommandArgumentContext(CommandArgumentContextBuilder().apply(block).distinctByReversed { it.klass }) +public fun buildCommandArgumentContext(block: CommandArgumentContextBuilder.() -> Unit): CommandArgumentContext { + return CommandArgumentContextBuilder().apply(block).build() } /** - * @see CommandArgumentContext + * 参考 [buildCommandArgumentContext] */ public class CommandArgumentContextBuilder : MutableList> by mutableListOf() { - @JvmName("add") // TODO: 2020/8/19 java class support - public inline infix fun KClass.with(parser: CommandArgumentParser): ParserPair<*> = - ParserPair(this, parser).also { add(it) } + /** + * 添加一个指令解析器. + */ + @JvmName("add") + public infix fun Class.with(parser: CommandArgumentParser): CommandArgumentContextBuilder = + this.kotlin with parser + + /** + * 添加一个指令解析器 + */ + @JvmName("add") + public inline infix fun KClass.with(parser: CommandArgumentParser): CommandArgumentContextBuilder { + add(ParserPair(this, parser)) + return this@CommandArgumentContextBuilder + } /** * 添加一个指令解析器 @@ -179,7 +208,12 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @LowPriorityInOverloadResolution public inline infix fun KClass.with( crossinline parser: CommandArgumentParser.(s: String, sender: CommandSender) -> T - ): ParserPair<*> = ParserPair(this, CommandArgumentParser(parser)).also { add(it) } + ): CommandArgumentContextBuilder { + add(ParserPair(this, object : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) + })) + return this@CommandArgumentContextBuilder + } /** * 添加一个指令解析器 @@ -187,12 +221,18 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @JvmSynthetic public inline infix fun KClass.with( crossinline parser: CommandArgumentParser.(s: String) -> T - ): ParserPair<*> = - ParserPair(this, CommandArgumentParser { s: String, _: CommandSender -> parser(s) }).also { add(it) } + ): CommandArgumentContextBuilder { + add(ParserPair(this, object : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): T = parser(raw) + })) + return this@CommandArgumentContextBuilder + } @JvmSynthetic - public inline fun add(parser: CommandArgumentParser): ParserPair<*> = - ParserPair(T::class, parser).also { add(it) } + public inline fun add(parser: CommandArgumentParser): CommandArgumentContextBuilder { + add(ParserPair(T::class, parser)) + return this@CommandArgumentContextBuilder + } /** * 添加一个指令解析器 @@ -201,7 +241,9 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @JvmSynthetic public inline infix fun add( crossinline parser: CommandArgumentParser<*>.(s: String) -> T - ): ParserPair<*> = T::class with CommandArgumentParser { s: String, _: CommandSender -> parser(s) } + ): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): T = parser(raw) + } /** * 添加一个指令解析器 @@ -211,7 +253,14 @@ public class CommandArgumentContextBuilder : MutableList> by mutab @LowPriorityInOverloadResolution public inline infix fun add( crossinline parser: CommandArgumentParser<*>.(s: String, sender: CommandSender) -> T - ): ParserPair<*> = T::class with CommandArgumentParser(parser) + ): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) + } + + /** + * 完成构建, 得到 [CommandArgumentContext] + */ + public fun build(): CommandArgumentContext = SimpleCommandArgumentContext(this.distinctByReversed { it.klass }) } internal inline fun List.distinctByReversed(selector: (T) -> K): List { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt index 3cef69b28..68bac374d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt @@ -11,27 +11,82 @@ package net.mamoe.mirai.console.command.description +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.CompositeCommand +import net.mamoe.mirai.console.command.SimpleCommand +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member import net.mamoe.mirai.message.data.SingleMessage import net.mamoe.mirai.message.data.content +import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * 指令参数解析器. + * 指令参数解析器. 用于解析字符串或 [SingleMessage] 到特定参数类型. * - * @see CommandArgumentContext - * @see ExistFriendArgumentParser + * ### 参数解析 + * + * 如 [SimpleCommand] 中的示例: + * ``` + * suspend fun CommandSender.mute(target: Member, duration: Int) + * ``` + * [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] 为 [Member] 的 [CommandArgumentParser], 并调用其 [CommandArgumentParser.parse] + * + * ### 内建指令解析器 + * - 基础类型: [ByteArgumentParser], [ShortArgumentParser], [IntArgumentParser], [LongArgumentParser] + * [FloatArgumentParser], [DoubleArgumentParser], + * [BooleanArgumentParser], [StringArgumentParser] + * + * - [Bot]: [ExistingBotArgumentParser] + * - [Friend]: [ExistingFriendArgumentParser] + * - [Group]: [ExistingGroupArgumentParser] + * - [Member]: [ExistMemberArgumentParser] + * + * + * @see SimpleCommand 简单指令 + * @see CompositeCommand 复合指令 + * + * @see buildCommandArgumentContext 指令参数环境, 即 [CommandArgumentParser] 的集合 */ public interface CommandArgumentParser { + /** + * 解析一个字符串为 [T] 类型参数 + * + * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. + * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. + * + * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. + * + * @see CommandArgumentParserException + */ + @Throws(CommandArgumentParserException::class) public fun parse(raw: String, sender: CommandSender): T + /** + * 解析一个字符串为 [T] 类型参数 + * + * **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException]. + * 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户. + * + * @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出. + * + * @see CommandArgumentParserException + */ + @Throws(CommandArgumentParserException::class) @JvmDefault public fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender) } /** - * 解析 + * 解析一个字符串或 [SingleMessage] 为 [T] 类型参数 + * + * @throws IllegalArgumentException 当 [raw] 既不是 [SingleMessage], 也不是 [String] 时抛出. */ +@JvmSynthetic +@Throws(IllegalArgumentException::class) public fun CommandArgumentParser.parse(raw: Any, sender: CommandSender): T { contract { returns() implies (raw is String || raw is SingleMessage) @@ -45,14 +100,23 @@ public fun CommandArgumentParser.parse(raw: Any, sender: CommandSen } /** - * 抛出一个 [CommandArgumentParserException] + * 抛出一个 [CommandArgumentParserException] 的捷径 + * + * @throws CommandArgumentParserException */ @Suppress("unused") @JvmSynthetic +@Throws(CommandArgumentParserException::class) public inline fun CommandArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { throw CommandArgumentParserException(message, cause) } +/** + * 检查参数 [condition]. 当它为 `false` 时调用 [message] 并以其返回值作为消息, 抛出异常 [CommandArgumentParserException] + * + * @throws CommandArgumentParserException + */ +@Throws(CommandArgumentParserException::class) @JvmSynthetic public inline fun CommandArgumentParser<*>.checkArgument( condition: Boolean, @@ -60,10 +124,13 @@ public inline fun CommandArgumentParser<*>.checkArgument( ) { contract { returns() implies condition + callsInPlace(message, InvocationKind.AT_MOST_ONCE) } if (!condition) illegalArgument(message()) } +/* + /** * 创建匿名 [CommandArgumentParser] */ @@ -88,3 +155,4 @@ public inline fun CommandArgumentParser( override fun parse(raw: SingleMessage, sender: CommandSender): T = messageParser(raw, sender) } +*/ \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt similarity index 94% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManagerImpl.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt index f01c0c55b..869a2a0f7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt @@ -7,16 +7,14 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.command +package net.mamoe.mirai.console.internal.command import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.Command.Companion.primaryName -import net.mamoe.mirai.console.internal.command.executeCommandInternal -import net.mamoe.mirai.console.internal.command.flattenCommandComponents -import net.mamoe.mirai.console.internal.command.intersectsIgnoringCase import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.message.MessageEvent @@ -88,7 +86,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine ///// IMPL - override val CommandOwner.registeredCommands: List get() = this@CommandManagerImpl.registeredCommands.filter { it.owner == this } + override val CommandOwner.registeredCommands: List get() = CommandManagerImpl.registeredCommands.filter { it.owner == this } override val allRegisteredCommands: List get() = registeredCommands.toList() // copy override val commandPrefix: String get() = "/" override fun CommandOwner.unregisterAllCommands() { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt similarity index 93% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt index 6c8ee69f0..dd89c92e6 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommand.CommandParam.kt @@ -9,9 +9,10 @@ @file:Suppress("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") -package net.mamoe.mirai.console.command.description +package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.CompositeCommand +import net.mamoe.mirai.console.command.description.CommandArgumentParser import java.lang.reflect.Parameter import kotlin.reflect.KClass diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt index e19db7a2d..88308b5fb 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt @@ -14,7 +14,6 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentContextAware -import net.mamoe.mirai.console.command.description.CommandParameter import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.SingleMessage import kotlin.reflect.KAnnotatedElement diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt index 11774a521..f03fd34f8 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt @@ -109,18 +109,14 @@ internal inline fun List.dropToTypedArray(n: Int): Array = Arr @JvmSynthetic @Throws(CommandExecutionException::class) -internal suspend inline fun CommandSender.executeCommandInternal( +internal suspend fun CommandSender.executeCommandInternal( command: Command, args: Array, commandName: String, checkPermission: Boolean ) { if (checkPermission && !command.testPermission(this)) { - throw CommandExecutionException( - command, - commandName, - CommandPermissionDeniedException(command) - ) + throw CommandExecutionException(this, command, commandName, CommandPermissionDeniedException(this, command)) } kotlin.runCatching { @@ -128,7 +124,7 @@ internal suspend inline fun CommandSender.executeCommandInternal( }.onFailure { catchExecutionException(it) if (it !is CommandArgumentParserException) { - throw CommandExecutionException(command, commandName, it) + throw CommandExecutionException(this, command, commandName, it) } } } diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index b774aed31..23f3918c9 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -22,8 +22,8 @@ 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.unregisterAllCommands -import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentParser +import net.mamoe.mirai.console.command.description.buildCommandArgumentContext import net.mamoe.mirai.console.initTestEnvironment import net.mamoe.mirai.console.internal.command.flattenCommandComponents import net.mamoe.mirai.message.data.Image @@ -187,7 +187,7 @@ internal class TestCommand { val composite = object : CompositeCommand( ConsoleCommandOwner, "test", - overrideContext = CommandArgumentContext { + overrideContext = buildCommandArgumentContext { add(object : CommandArgumentParser { override fun parse(raw: String, sender: CommandSender): MyClass { return MyClass(raw.toInt())