Merge pull request #190 from mamoe/command

重构 command 系统
This commit is contained in:
Him188 2020-10-24 21:23:56 +08:00 committed by GitHub
commit 15d0cdaf90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 2093 additions and 941 deletions

View File

@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands
import net.mamoe.mirai.console.internal.util.runIgnoreException
@ -150,8 +150,8 @@ public object BuiltInCommands {
ConsoleCommandOwner, "permission", "权限", "perm",
description = "管理权限",
overrideContext = buildCommandArgumentContext {
PermitteeId::class with PermitteeIdArgumentParser
Permission::class with PermissionIdArgumentParser.map { id ->
PermitteeId::class with PermitteeIdValueArgumentParser
Permission::class with PermissionIdValueArgumentParser.map { id ->
kotlin.runCatching {
id.findCorrespondingPermissionOrFail()
}.getOrElse { illegalArgument("指令不存在: $id", it) }

View File

@ -11,25 +11,27 @@
package net.mamoe.mirai.console.command
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
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
import net.mamoe.mirai.console.command.java.JCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* 指令
*
* @see CommandManager.register 注册这个指令
* @see CommandManager.registerCommand 注册这个指令
*
* @see RawCommand 无参数解析, 接收原生参数的指令
* @see CompositeCommand 复合指令
* @see SimpleCommand 简单的, 支持参数自动解析的指令
*
* @see CommandArgumentContextAware
*
* @see JCommand Java 用户添加协程帮助的 [Command]
*/
public interface Command {
@ -48,18 +50,25 @@ public interface Command {
@ResolveContext(COMMAND_NAME)
public val secondaryNames: Array<out String>
/**
* 指令可能的参数列表.
*/
@ConsoleExperimentalApi("Property name is experimental")
@ExperimentalCommandDescriptors
public val overloads: List<CommandSignatureVariant>
/**
* 用法说明, 用于发送给用户. [usage] 一般包含 [description].
*/
public val usage: String
/**
* 指令描述, 用于显示在 [BuiltInCommands.HelpCommand]
* 描述, 用于显示在 [BuiltInCommands.HelpCommand]
*/
public val description: String
/**
* 此指令分配的权限.
* 此指令分配的权限.
*
* ### 实现约束
* - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace]
@ -72,6 +81,8 @@ public interface Command {
*
* 会影响聊天语境中的解析.
*/
@ExperimentalCommandDescriptors
@ConsoleExperimentalApi
public val prefixOptional: Boolean
/**
@ -80,16 +91,6 @@ public interface Command {
*/
public val owner: CommandOwner
/**
* 在指令被执行时调用.
*
* @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数.
*
* @see CommandManager.executeCommand 查看更多信息
*/
@JvmBlockingBridge
public suspend fun CommandSender.onCommand(args: MessageChain)
public companion object {
/**
@ -109,19 +110,10 @@ 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.")
}
}
}
}
/**
* 调用 [Command.onCommand]
* @see Command.onCommand
*/
@JvmSynthetic
public suspend inline fun Command.onCommand(sender: CommandSender, args: MessageChain): Unit =
sender.onCommand(args)

View File

@ -12,6 +12,8 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import kotlin.contracts.contract
@ -21,6 +23,8 @@ import kotlin.contracts.contract
*
* @see CommandExecuteStatus
*/
@ConsoleExperimentalApi("Not yet implemented")
@ExperimentalCommandDescriptors
public sealed class CommandExecuteResult {
/** 指令最终执行状态 */
public abstract val status: CommandExecuteStatus
@ -55,6 +59,21 @@ public sealed class CommandExecuteResult {
public override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL
}
/** 执行执行时发生了一个非法参数错误 */
public class IllegalArgument(
/** 指令执行时发生的错误 */
public override val exception: IllegalCommandArgumentException,
/** 尝试执行的指令 */
public override val command: Command,
/** 尝试执行的指令名 */
public override val commandName: String,
/** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */
public override val args: MessageChain
) : CommandExecuteResult() {
/** 指令最终执行状态, 总是 [CommandExecuteStatus.EXECUTION_EXCEPTION] */
public override val status: CommandExecuteStatus get() = CommandExecuteStatus.ILLEGAL_ARGUMENT
}
/** 指令执行过程出现了错误 */
public class ExecutionFailed(
/** 指令执行时发生的错误 */
@ -71,9 +90,9 @@ public sealed class CommandExecuteResult {
}
/** 没有匹配的指令 */
public class CommandNotFound(
public class UnresolvedCall(
/** 尝试执行的指令名 */
public override val commandName: String
public override val commandName: String,
) : CommandExecuteResult() {
/** 指令执行时发生的错误, 总是 `null` */
public override val exception: Nothing? get() = null
@ -119,7 +138,9 @@ public sealed class CommandExecuteResult {
COMMAND_NOT_FOUND,
/** 权限不足 */
PERMISSION_DENIED
PERMISSION_DENIED,
/** 非法参数 */
ILLEGAL_ARGUMENT,
}
}
@ -138,6 +159,18 @@ public fun CommandExecuteResult.isSuccess(): Boolean {
return this is CommandExecuteResult.Success
}
/**
* [this] [CommandExecuteResult.IllegalArgument] 时返回 `true`
*/
@JvmSynthetic
public fun CommandExecuteResult.isIllegalArgument(): Boolean {
contract {
returns(true) implies (this@isIllegalArgument is CommandExecuteResult.IllegalArgument)
returns(false) implies (this@isIllegalArgument !is CommandExecuteResult.IllegalArgument)
}
return this is CommandExecuteResult.IllegalArgument
}
/**
* [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true`
*/
@ -151,7 +184,7 @@ public fun CommandExecuteResult.isExecutionException(): Boolean {
}
/**
* [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true`
* [this] [CommandExecuteResult.PermissionDenied] 时返回 `true`
*/
@JvmSynthetic
public fun CommandExecuteResult.isPermissionDenied(): Boolean {
@ -163,19 +196,19 @@ public fun CommandExecuteResult.isPermissionDenied(): Boolean {
}
/**
* [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true`
* [this] [CommandExecuteResult.UnresolvedCall] 时返回 `true`
*/
@JvmSynthetic
public fun CommandExecuteResult.isCommandNotFound(): Boolean {
contract {
returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound)
returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound)
returns(true) implies (this@isCommandNotFound is CommandExecuteResult.UnresolvedCall)
returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.UnresolvedCall)
}
return this is CommandExecuteResult.CommandNotFound
return this is CommandExecuteResult.UnresolvedCall
}
/**
* [this] [CommandExecuteResult.ExecutionFailed] [CommandExecuteResult.CommandNotFound] 时返回 `true`
* [this] [CommandExecuteResult.ExecutionFailed], [CommandExecuteResult.IllegalArgument] [CommandExecuteResult.UnresolvedCall] 时返回 `true`
*/
@JvmSynthetic
public fun CommandExecuteResult.isFailure(): Boolean {

View File

@ -8,7 +8,7 @@
*/
@file:Suppress(
"NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE",
"NOTHING_TO_INLINE", "unused",
"MemberVisibilityCanBePrivate", "INAPPLICABLE_JVM_NAME"
)
@file:JvmName("CommandManagerKt")
@ -16,21 +16,21 @@
package net.mamoe.mirai.console.command
import net.mamoe.kjbb.JvmBlockingBridge
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.resolve.CommandCallResolver
import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand
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>
/**
* 获取所有已经注册了指令列表.
*
@ -44,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)
/**
* 注册一个指令.
@ -65,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] 分割.
@ -101,19 +105,113 @@ public interface CommandManager {
* 注意: 字符串与消息元素之间不需要空格, 会被强制分割. "bar[mirai:image:]" 会被分割为 "bar" [Image] 类型的消息元素.
* 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand]
*
* ### 未来的扩展
* 在将来, 参数语法分析过程可能会被扩展, 允许插件自定义处理方式, 因此可能不会简单地使用空格分隔.
* ### 扩展
* 参数语法分析过程可能会被扩展, 插件可以自定义处理方式 ([CommandCallParser]), 因此可能不会简单地使用空格分隔.
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @see CommandCallParser
* @see CommandCallResolver
*
* @see CommandSender.executeCommand
* @see Command.execute
*
* @return 执行结果
*/
@ExperimentalCommandDescriptors
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(
public suspend fun executeCommand(
caller: CommandSender,
message: Message,
checkPermission: Boolean = true,
): CommandExecuteResult
): CommandExecuteResult {
return executeCommandImpl(message, caller, checkPermission)
}
/**
* 执行一个确切的指令
*
* @param command 目标指令
* @param arguments 参数列表
*
* @see executeCommand 获取更多信息
* @see Command.execute
*/
@ConsoleExperimentalApi
@JvmName("executeCommand")
@ExperimentalCommandDescriptors
@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 CommandManager.executeCommand(sender, chain, checkPermission)
}
/**
* [指令名称][commandName] 匹配对应的 [Command].
*
* #### 实现细节
* - [commandName] 带有 [commandPrefix] 时可以匹配到所有指令
* - [commandName] 不带有 [commandPrefix] 时只能匹配到 [Command.prefixOptional] 的指令
*
* @param commandName 可能带有或不带有 [commandPrefix].
*/
public fun matchCommand(commandName: String): Command?
public companion object INSTANCE : CommandManager by CommandManagerImpl {
/**
* @see CommandManager.getRegisteredCommands
*/
@get:JvmName("registeredCommands0")
@get:JvmSynthetic
public inline val CommandOwner.registeredCommands: List<Command>
get() = getRegisteredCommands(this)
/**
* @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)
}
}
/**
* 解析并执行一个指令
@ -124,95 +222,37 @@ public interface CommandManager {
* @return 执行结果
* @see executeCommand
*/
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true,
): CommandExecuteResult = executeCommand(PlainText(message).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
): CommandExecuteResult = CommandManager.executeCommand(sender, this, arguments, checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmBlockingBridge
@JvmName("executeCommand")
public suspend fun Command.execute(
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline 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
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
override suspend fun Command.execute(
sender: CommandSender,
arguments: Message,
checkPermission: Boolean,
): CommandExecuteResult =
CommandManagerImpl.run { execute(sender, arguments = arguments, checkPermission = checkPermission) }
override suspend fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean,
): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) }
override suspend fun Command.execute(
sender: CommandSender,
arguments: String,
checkPermission: Boolean,
): CommandExecuteResult = CommandManagerImpl.run { execute(sender, arguments, checkPermission) }
override suspend fun CommandSender.executeCommand(
message: Message,
checkPermission: Boolean,
): CommandExecuteResult = CommandManagerImpl.run { executeCommand(message, checkPermission) }
/**
* 执行一个确切的指令
* @see execute 获取更多信息
*/
public suspend fun CommandSender.execute(
command: Command,
arguments: Message,
checkPermission: Boolean = true,
): CommandExecuteResult {
return command.execute(this, arguments, checkPermission)
}
/**
* 执行一个确切的指令
* @see execute 获取更多信息
*/
public suspend fun CommandSender.execute(
command: Command,
arguments: String,
checkPermission: Boolean = true,
): CommandExecuteResult {
return command.execute(this, arguments, checkPermission)
}
}
}
): CommandExecuteResult = execute(sender, PlainText(arguments), checkPermission)

View File

@ -20,15 +20,14 @@ import kotlinx.coroutines.launch
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.CommandSender.Companion.asCommandSender
import net.mamoe.mirai.console.command.CommandSender.Companion.asMemberCommandSender
import net.mamoe.mirai.console.command.CommandSender.Companion.asTempCommandSender
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.command.description.CommandArgumentParserException
import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.data.castOrNull
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.plugin.rootCauseOrSelf
import net.mamoe.mirai.console.permission.AbstractPermitteeId
import net.mamoe.mirai.console.permission.Permittee
@ -281,6 +280,13 @@ public sealed class AbstractCommandSender : CommandSender, CoroutineScope {
if (this is CommandSenderOnMessage<*>) {
val cause = e.rootCauseOrSelf
// TODO: 2020/10/17
// CommandArgumentParserException 作为 IllegalCommandArgumentException 不会再进入此函数
// 已在
// - [console] CommandManagerImpl.commandListener
// - [terminal] ConsoleThread.kt
// 处理
val message = cause
.takeIf { it is CommandArgumentParserException }?.message
?: "${cause::class.simpleName.orEmpty()}: ${cause.message}"

View File

@ -17,14 +17,13 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand
import net.mamoe.mirai.console.internal.command.CommandReflector
import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FUNCTION
@ -90,16 +89,26 @@ public abstract class CompositeCommand(
parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
reflector.findSubCommands()
}
/**
* 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖.
*/
public override val usage: String get() = super.usage
public override val usage: String by lazy {
@OptIn(ExperimentalCommandDescriptors::class)
reflector.generateUsage(overloads)
}
/**
* [CommandArgumentParser] 的环境
* [CommandValueArgumentParser] 的环境
*/
public final override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext
@ -123,20 +132,6 @@ public abstract class CompositeCommand(
@Retention(RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
protected annotation class Name(val value: String)
public final override suspend fun CommandSender.onCommand(args: MessageChain) {
matchSubCommand(args)?.parseAndExecute(this, args, true) ?: kotlin.run {
defaultSubCommand.onCommand(this, args)
}
}
protected override suspend fun CommandSender.onDefault(rawArgs: MessageChain) {
sendMessage(usage)
}
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = CompositeCommandSubCommandAnnotationResolver
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException
/**
* 在处理参数时遇到的 _正常_ 错误. 如参数不符合规范, 参数值越界等.
*
* [message] 将会发送给指令调用方.
*
* @see CommandArgumentParserException
*/
public open class IllegalCommandArgumentException : IllegalArgumentException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}

View File

@ -11,14 +11,17 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.java.JRawCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.buildMessageChain
/**
* 无参数解析, 接收原生参数的指令.
@ -52,6 +55,18 @@ public abstract class RawCommand(
) : Command {
public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) }
@ExperimentalCommandDescriptors
override val overloads: List<CommandSignatureVariant> = listOf(
CommandSignatureVariantImpl(
receiverParameter = CommandReceiverParameter(false, typeOf0<CommandSender>()),
valueParameters = listOf(AbstractCommandValueParameter.UserDefinedType.createRequired<Array<out Message>>("args", true))
) { call ->
val sender = call.caller
val arguments = call.rawValueArguments
sender.onCommand(buildMessageChain { arguments.forEach { +it.value } })
}
)
/**
* 在指令被执行时调用.
*
@ -59,7 +74,7 @@ public abstract class RawCommand(
*
* @see CommandManager.execute 查看更多信息
*/
public abstract override suspend fun CommandSender.onCommand(args: MessageChain)
public abstract suspend fun CommandSender.onCommand(args: MessageChain)
}

View File

@ -18,20 +18,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.command.descriptor.*
import net.mamoe.mirai.console.command.java.JSimpleCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand
import net.mamoe.mirai.console.internal.command.CommandReflector
import net.mamoe.mirai.console.internal.command.IllegalCommandDeclarationException
import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
/**
* 简单的, 支持参数自动解析的指令.
*
* 要查看指令解析流程, 参考 [CommandManager.executeCommand]
* 要查看参数解析方式, 参考 [CommandArgumentParser]
* 要查看参数解析方式, 参考 [CommandValueArgumentParser]
*
* Kotlin 实现:
* ```
@ -58,39 +61,41 @@ public abstract class SimpleCommand(
parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureVariantFromKFunction> by lazy {
reflector.findSubCommands().also {
if (it.isEmpty())
throw IllegalCommandDeclarationException(this, "SimpleCommand must have at least one subcommand, whereas zero present.")
}
}
/**
* 自动根据带有 [Handler] 注解的函数签名生成 [usage]. 也可以被覆盖.
*/
public override val usage: String get() = super.usage
public override val usage: String by lazy {
@OptIn(ExperimentalCommandDescriptors::class)
reflector.generateUsage(overloads)
}
/**
* 标注指令处理器
*/
@Target(FUNCTION)
protected annotation class Handler
/** 参数名, 将参与构成 [usage] */
@ConsoleExperimentalApi("Classname might change")
@Target(VALUE_PARAMETER)
protected annotation class Name(val value: String)
/**
* 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext`
*/
public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext
public final override suspend fun CommandSender.onCommand(args: MessageChain) {
subCommands.single().parseAndExecute(this, args, false)
}
internal override fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) {
super.checkSubCommand(subCommands)
check(subCommands.size == 1) { "There can only be exactly one function annotated with Handler at this moment as overloading is not yet supported." }
}
@Deprecated("prohibited", level = DeprecationLevel.HIDDEN)
internal override suspend fun CommandSender.onDefault(rawArgs: MessageChain) {
sendMessage(usage)
}
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = SimpleCommandSubCommandAnnotationResolver
}

View File

@ -9,18 +9,19 @@
@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.command.description
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.Bot
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.console.command.description.CommandArgumentContext.ParserPair
import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext.ParserPair
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.PlainText
import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass
@ -28,7 +29,7 @@ import kotlin.reflect.full.isSubclassOf
/**
* 指令参数环境, [CommandArgumentParser] 的集合, 用于 [CompositeCommand] [SimpleCommand].
* 指令参数环境, [CommandValueArgumentParser] 的集合, 用于 [CompositeCommand] [SimpleCommand].
*
* 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器
*
@ -37,20 +38,20 @@ import kotlin.reflect.full.isSubclassOf
* @see SimpleCommandArgumentContext 简单实现
* @see EmptyCommandArgumentContext 空实现, 类似 [emptyList]
*
* @see CommandArgumentContext.Builtins 内建 [CommandArgumentParser]
* @see CommandArgumentContext.Builtins 内建 [CommandValueArgumentParser]
*
* @see buildCommandArgumentContext DSL 构造
*/
public interface CommandArgumentContext {
/**
* [KClass] [CommandArgumentParser] 的匹配
* [KClass] [CommandValueArgumentParser] 的匹配
*/
public data class ParserPair<T : Any>(
val klass: KClass<T>,
val parser: CommandArgumentParser<T>,
val parser: CommandValueArgumentParser<T>,
)
public operator fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>?
public operator fun <T : Any> get(klass: KClass<out T>): CommandValueArgumentParser<T>?
public fun toList(): List<ParserPair<*>>
@ -65,30 +66,32 @@ public interface CommandArgumentContext {
}
/**
* 内建的默认 [CommandArgumentParser]
* 内建的默认 [CommandValueArgumentParser]
*/
public object Builtins : CommandArgumentContext by (buildCommandArgumentContext {
Int::class with IntArgumentParser
Byte::class with ByteArgumentParser
Short::class with ShortArgumentParser
Boolean::class with BooleanArgumentParser
String::class with StringArgumentParser
Long::class with LongArgumentParser
Double::class with DoubleArgumentParser
Float::class with FloatArgumentParser
Int::class with IntValueArgumentParser
Byte::class with ByteValueArgumentParser
Short::class with ShortValueArgumentParser
Boolean::class with BooleanValueArgumentParser
String::class with StringValueArgumentParser
Long::class with LongValueArgumentParser
Double::class with DoubleValueArgumentParser
Float::class with FloatValueArgumentParser
Image::class with ImageArgumentParser
PlainText::class with PlainTextArgumentParser
Image::class with ImageValueArgumentParser
PlainText::class with PlainTextValueArgumentParser
Contact::class with ExistingContactArgumentParser
User::class with ExistingUserArgumentParser
Member::class with ExistingMemberArgumentParser
Group::class with ExistingGroupArgumentParser
Friend::class with ExistingFriendArgumentParser
Bot::class with ExistingBotArgumentParser
Contact::class with ExistingContactValueArgumentParser
User::class with ExistingUserValueArgumentParser
Member::class with ExistingMemberValueArgumentParser
Group::class with ExistingGroupValueArgumentParser
Friend::class with ExistingFriendValueArgumentParser
Bot::class with ExistingBotValueArgumentParser
PermissionId::class with PermissionIdArgumentParser
PermitteeId::class with PermitteeIdArgumentParser
PermissionId::class with PermissionIdValueArgumentParser
PermitteeId::class with PermitteeIdValueArgumentParser
MessageContent::class with RawContentValueArgumentParser
})
}
@ -100,7 +103,7 @@ public interface CommandArgumentContext {
*/
public interface CommandArgumentContextAware {
/**
* [CommandArgumentParser] 的集合
* [CommandValueArgumentParser] 的集合
*/
public val context: CommandArgumentContext
}
@ -114,7 +117,7 @@ public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext
if (replacer == EmptyCommandArgumentContext) return this
if (this == EmptyCommandArgumentContext) return replacer
return object : CommandArgumentContext {
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? =
override fun <T : Any> get(klass: KClass<out T>): CommandValueArgumentParser<T>? =
replacer[klass] ?: this@plus[klass]
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
@ -129,8 +132,8 @@ public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>):
if (this == EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer)
return object : CommandArgumentContext {
@Suppress("UNCHECKED_CAST")
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? =
replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgumentParser<T>?
override fun <T : Any> get(klass: KClass<out T>): CommandValueArgumentParser<T>? =
replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandValueArgumentParser<T>?
?: this@plus[klass]
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
@ -146,9 +149,9 @@ public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>):
public class SimpleCommandArgumentContext(
public val list: List<ParserPair<*>>,
) : CommandArgumentContext {
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? =
override fun <T : Any> get(klass: KClass<out T>): CommandValueArgumentParser<T>? =
(this.list.firstOrNull { klass == it.klass }?.parser
?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandArgumentParser<T>?
?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandValueArgumentParser<T>?
override fun toList(): List<ParserPair<*>> = list
}
@ -160,7 +163,7 @@ public class SimpleCommandArgumentContext(
* ```
* val context = buildCommandArgumentContext {
* Int::class with IntArgParser
* Member::class with ExistMemberArgParser
* Member::class with ExistingMemberArgParser
* Group::class with { s: String, sender: CommandSender ->
* Bot.getInstance(s.toLong()).getGroup(s.toLong())
* }
@ -200,14 +203,14 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
* 添加一个指令解析器.
*/
@JvmName("add")
public infix fun <T : Any> Class<T>.with(parser: CommandArgumentParser<T>): CommandArgumentContextBuilder =
public infix fun <T : Any> Class<T>.with(parser: CommandValueArgumentParser<T>): CommandArgumentContextBuilder =
this.kotlin with parser
/**
* 添加一个指令解析器
*/
@JvmName("add")
public inline infix fun <T : Any> KClass<T>.with(parser: CommandArgumentParser<T>): CommandArgumentContextBuilder {
public inline infix fun <T : Any> KClass<T>.with(parser: CommandValueArgumentParser<T>): CommandArgumentContextBuilder {
add(ParserPair(this, parser))
return this@CommandArgumentContextBuilder
}
@ -218,9 +221,9 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@JvmSynthetic
@LowPriorityInOverloadResolution
public inline infix fun <T : Any> KClass<T>.with(
crossinline parser: CommandArgumentParser<T>.(s: String, sender: CommandSender) -> T,
crossinline parser: CommandValueArgumentParser<T>.(s: String, sender: CommandSender) -> T,
): CommandArgumentContextBuilder {
add(ParserPair(this, object : CommandArgumentParser<T> {
add(ParserPair(this, object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
}))
return this@CommandArgumentContextBuilder
@ -231,16 +234,16 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
*/
@JvmSynthetic
public inline infix fun <T : Any> KClass<T>.with(
crossinline parser: CommandArgumentParser<T>.(s: String) -> T,
crossinline parser: CommandValueArgumentParser<T>.(s: String) -> T,
): CommandArgumentContextBuilder {
add(ParserPair(this, object : CommandArgumentParser<T> {
add(ParserPair(this, object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw)
}))
return this@CommandArgumentContextBuilder
}
@JvmSynthetic
public inline fun <reified T : Any> add(parser: CommandArgumentParser<T>): CommandArgumentContextBuilder {
public inline fun <reified T : Any> add(parser: CommandValueArgumentParser<T>): CommandArgumentContextBuilder {
add(ParserPair(T::class, parser))
return this@CommandArgumentContextBuilder
}
@ -251,8 +254,8 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@ConsoleExperimentalApi
@JvmSynthetic
public inline infix fun <reified T : Any> add(
crossinline parser: CommandArgumentParser<*>.(s: String) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser<T> {
crossinline parser: CommandValueArgumentParser<*>.(s: String) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw)
}
@ -263,8 +266,8 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@JvmSynthetic
@LowPriorityInOverloadResolution
public inline infix fun <reified T : Any> add(
crossinline parser: CommandArgumentParser<*>.(s: String, sender: CommandSender) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser<T> {
crossinline parser: CommandValueArgumentParser<*>.(s: String, sender: CommandSender) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
}

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.command.description
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.*
@ -25,47 +25,47 @@ import net.mamoe.mirai.message.data.*
/**
* 使用 [String.toInt] 解析
*/
public object IntArgumentParser : InternalCommandArgumentParserExtensions<Int> {
public object IntValueArgumentParser : InternalCommandValueArgumentParserExtensions<Int> {
public override fun parse(raw: String, sender: CommandSender): Int =
raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toLong] 解析
*/
public object LongArgumentParser : InternalCommandArgumentParserExtensions<Long> {
public object LongValueArgumentParser : InternalCommandValueArgumentParserExtensions<Long> {
public override fun parse(raw: String, sender: CommandSender): Long =
raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toShort] 解析
*/
public object ShortArgumentParser : InternalCommandArgumentParserExtensions<Short> {
public object ShortValueArgumentParser : InternalCommandValueArgumentParserExtensions<Short> {
public override fun parse(raw: String, sender: CommandSender): Short =
raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toByte] 解析
*/
public object ByteArgumentParser : InternalCommandArgumentParserExtensions<Byte> {
public object ByteValueArgumentParser : InternalCommandValueArgumentParserExtensions<Byte> {
public override fun parse(raw: String, sender: CommandSender): Byte =
raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toDouble] 解析
*/
public object DoubleArgumentParser : InternalCommandArgumentParserExtensions<Double> {
public object DoubleValueArgumentParser : InternalCommandValueArgumentParserExtensions<Double> {
public override fun parse(raw: String, sender: CommandSender): Double =
raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toFloat] 解析
*/
public object FloatArgumentParser : InternalCommandArgumentParserExtensions<Float> {
public object FloatValueArgumentParser : InternalCommandValueArgumentParserExtensions<Float> {
public override fun parse(raw: String, sender: CommandSender): Float =
raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
@ -73,14 +73,14 @@ public object FloatArgumentParser : InternalCommandArgumentParserExtensions<Floa
/**
* 直接返回 [String], 或取用 [SingleMessage.contentToString]
*/
public object StringArgumentParser : InternalCommandArgumentParserExtensions<String> {
public object StringValueArgumentParser : InternalCommandValueArgumentParserExtensions<String> {
public override fun parse(raw: String, sender: CommandSender): String = raw
}
/**
* 解析 [String] 通过 [Image].
*/
public object ImageArgumentParser : InternalCommandArgumentParserExtensions<Image> {
public object ImageValueArgumentParser : InternalCommandValueArgumentParserExtensions<Image> {
public override fun parse(raw: String, sender: CommandSender): Image {
return kotlin.runCatching {
Image(raw)
@ -95,7 +95,7 @@ public object ImageArgumentParser : InternalCommandArgumentParserExtensions<Imag
}
}
public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions<PlainText> {
public object PlainTextValueArgumentParser : InternalCommandValueArgumentParserExtensions<PlainText> {
public override fun parse(raw: String, sender: CommandSender): PlainText {
return PlainText(raw)
}
@ -109,7 +109,7 @@ public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions<
/**
* 当字符串内容为(不区分大小写) "true", "yes", "enabled"
*/
public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> {
public object BooleanValueArgumentParser : InternalCommandValueArgumentParserExtensions<Boolean> {
public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true)
@ -121,7 +121,7 @@ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Bo
/**
* 根据 [Bot.id] 解析一个登录后的 [Bot]
*/
public object ExistingBotArgumentParser : InternalCommandArgumentParserExtensions<Bot> {
public object ExistingBotValueArgumentParser : InternalCommandValueArgumentParserExtensions<Bot> {
public override fun parse(raw: String, sender: CommandSender): Bot =
if (raw == "~") sender.inferBotOrFail()
else raw.findBotOrFail()
@ -136,7 +136,7 @@ public object ExistingBotArgumentParser : InternalCommandArgumentParserExtension
/**
* 解析任意一个存在的好友.
*/
public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions<Friend> {
public object ExistingFriendValueArgumentParser : InternalCommandValueArgumentParserExtensions<Friend> {
private val syntax = """
- `botId.friendId`
- `botId.friendNick` (模糊搜索, 寻找最优匹配)
@ -175,7 +175,7 @@ public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtens
/**
* 解析任意一个存在的群.
*/
public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions<Group> {
public object ExistingGroupValueArgumentParser : InternalCommandValueArgumentParserExtensions<Group> {
private val syntax = """
- `botId.groupId`
- `~` (指代指令调用人自己所在群. 仅群聊天环境下)
@ -202,7 +202,7 @@ public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensi
}
}
public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensions<User> {
public object ExistingUserValueArgumentParser : InternalCommandValueArgumentParserExtensions<User> {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -215,11 +215,11 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
""".trimIndent()
override fun parse(raw: String, sender: CommandSender): User {
return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse)
return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse)
}
override fun parse(raw: MessageContent, sender: CommandSender): User {
return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse)
return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse)
}
private fun <T> parseImpl(
@ -246,7 +246,7 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
}
public object ExistingContactArgumentParser : InternalCommandArgumentParserExtensions<Contact> {
public object ExistingContactValueArgumentParser : InternalCommandValueArgumentParserExtensions<Contact> {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -259,11 +259,11 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
""".trimIndent()
override fun parse(raw: String, sender: CommandSender): Contact {
return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse)
return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse)
}
override fun parse(raw: MessageContent, sender: CommandSender): Contact {
return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse)
return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse)
}
private fun <T> parseImpl(
@ -286,7 +286,7 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
/**
* 解析任意一个群成员.
*/
public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtensions<Member> {
public object ExistingMemberValueArgumentParser : InternalCommandValueArgumentParserExtensions<Member> {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -333,7 +333,7 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens
}
}
public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> {
public object PermissionIdValueArgumentParser : CommandValueArgumentParser<PermissionId> {
override fun parse(raw: String, sender: CommandSender): PermissionId {
return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse {
illegalArgument("无法解析 $raw 为 PermissionId")
@ -341,7 +341,7 @@ public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> {
}
}
public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> {
public object PermitteeIdValueArgumentParser : CommandValueArgumentParser<PermitteeId> {
override fun parse(raw: String, sender: CommandSender): PermitteeId {
return if (raw == "~") sender.permitteeId
else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse {
@ -351,13 +351,19 @@ public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> {
override fun parse(raw: MessageContent, sender: CommandSender): PermitteeId {
if (raw is At) {
return ExistingUserArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId
return ExistingUserValueArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId
}
return super.parse(raw, sender)
}
}
internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> {
/** 直接返回原始参数 [MessageContent] */
public object RawContentValueArgumentParser : CommandValueArgumentParser<MessageContent> {
override fun parse(raw: String, sender: CommandSender): MessageContent = PlainText(raw)
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
}
internal interface InternalCommandValueArgumentParserExtensions<T : Any> : CommandValueArgumentParser<T> {
fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this")

View File

@ -9,17 +9,20 @@
@file:Suppress("unused")
package net.mamoe.mirai.console.command.description
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.IllegalCommandArgumentException
/**
* 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等.
*
* [message] 将会发送给指令调用方.
*
* @see CommandArgumentParser
* @see CommandArgumentParser.illegalArgument
* @see IllegalCommandArgumentException
* @see CommandValueArgumentParser
* @see CommandValueArgumentParser.illegalArgument
*/
public class CommandArgumentParserException : RuntimeException {
public class CommandArgumentParserException : IllegalCommandArgumentException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)

View File

@ -0,0 +1,287 @@
/*
* 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.descriptor
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createOptional
import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueParameter.UserDefinedType.Companion.createRequired
import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isAcceptable
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall
import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
/**
* @see CommandSignatureVariantImpl
*/
@ExperimentalCommandDescriptors
public interface CommandSignatureVariant {
@ConsoleExperimentalApi
public val receiverParameter: CommandReceiverParameter<out CommandSender>?
public val valueParameters: List<AbstractCommandValueParameter<*>>
public suspend fun call(resolvedCommandCall: ResolvedCommandCall)
}
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public interface CommandSignatureVariantFromKFunction : CommandSignatureVariant {
public val originFunction: KFunction<*>
}
@ExperimentalCommandDescriptors
public abstract class AbstractCommandSignatureVariant : CommandSignatureVariant {
override fun toString(): String {
val receiverParameter = receiverParameter
return if (receiverParameter == null) {
"CommandSignatureVariant(${valueParameters.joinToString()})"
} else {
"CommandSignatureVariant($receiverParameter, ${valueParameters.joinToString()})"
}
}
}
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
private val onCall: suspend CommandSignatureVariantImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariant, AbstractCommandSignatureVariant() {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}
}
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public open class CommandSignatureVariantFromKFunctionImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
override val originFunction: KFunction<*>,
private val onCall: suspend CommandSignatureVariantFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureVariantFromKFunction, AbstractCommandSignatureVariant() {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}
}
/**
* Inherited instances must be [CommandValueParameter] or [CommandReceiverParameter]
*/
@ExperimentalCommandDescriptors
public interface CommandParameter<T : Any?> {
public val name: String?
public val isOptional: Boolean
/**
* Reified type of [T]
*/
public val type: KType
}
@ExperimentalCommandDescriptors
public abstract class AbstractCommandParameter<T> : CommandParameter<T> {
override fun toString(): String = buildString {
append(name)
append(": ")
append(type.classifierAsKClass().simpleName)
append(if (type.isMarkedNullable) "?" else "")
}
}
/**
* Inherited instances must be [AbstractCommandValueParameter]
*/
@ExperimentalCommandDescriptors
public interface CommandValueParameter<T : Any?> : CommandParameter<T> {
public val isVararg: Boolean
public fun accepts(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): Boolean =
accepting(argument, commandArgumentContext).isAcceptable
public fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance
}
@ExperimentalCommandDescriptors
public sealed class ArgumentAcceptance(
/**
* Higher means more acceptable
*/
@ConsoleExperimentalApi
public val acceptanceLevel: Int,
) {
public object Direct : ArgumentAcceptance(Int.MAX_VALUE)
public class WithTypeConversion(
public val typeVariant: TypeVariant<*>,
) : ArgumentAcceptance(20)
public class WithContextualConversion(
public val parser: CommandValueArgumentParser<*>,
) : ArgumentAcceptance(10)
public class ResolutionAmbiguity(
public val candidates: List<TypeVariant<*>>,
) : ArgumentAcceptance(0)
public object Impossible : ArgumentAcceptance(-1)
public companion object {
@JvmStatic
public val ArgumentAcceptance.isAcceptable: Boolean
get() = acceptanceLevel > 0
@JvmStatic
public val ArgumentAcceptance.isNotAcceptable: Boolean
get() = acceptanceLevel <= 0
}
}
@ExperimentalCommandDescriptors
public class CommandReceiverParameter<T : CommandSender>(
override val isOptional: Boolean,
override val type: KType,
) : CommandParameter<T>, AbstractCommandParameter<T>() {
override val name: String get() = PARAMETER_NAME
init {
check(type.classifier is KClass<*>) {
"CommandReceiverParameter.type.classifier must be KClass."
}
}
public companion object {
public const val PARAMETER_NAME: String = "<receiver>"
}
}
internal val ANY_TYPE = typeOf0<Any>()
internal val ARRAY_OUT_ANY_TYPE = typeOf0<Array<out Any?>>()
@ExperimentalCommandDescriptors
public sealed class AbstractCommandValueParameter<T> : CommandValueParameter<T>, AbstractCommandParameter<T>() {
override fun toString(): String = buildString {
if (isVararg) append("vararg ")
append(super.toString())
if (isOptional) {
append(" = ...")
}
}
public override fun accepting(argument: CommandValueArgument, commandArgumentContext: CommandArgumentContext?): ArgumentAcceptance {
if (isVararg) {
val arrayElementType = this.type.arguments.single() // Array<T>
return acceptingImpl(arrayElementType.type ?: ANY_TYPE, argument, commandArgumentContext)
}
return acceptingImpl(this.type, argument, commandArgumentContext)
}
private fun acceptingImpl(
expectingType: KType,
argument: CommandValueArgument,
commandArgumentContext: CommandArgumentContext?,
): ArgumentAcceptance {
if (argument.type.isSubtypeOf(expectingType)) return ArgumentAcceptance.Direct
argument.typeVariants.associateWith { typeVariant ->
if (typeVariant.outType.isSubtypeOf(expectingType)) {
// TODO: 2020/10/11 resolution ambiguity
return ArgumentAcceptance.WithTypeConversion(typeVariant)
}
}
expectingType.classifierAsKClassOrNull()?.let { commandArgumentContext?.get(it) }?.let { parser ->
return ArgumentAcceptance.WithContextualConversion(parser)
}
return ArgumentAcceptance.Impossible
}
@ConsoleExperimentalApi
public class StringConstant(
@ConsoleExperimentalApi
public override val name: String?,
public val expectingValue: String,
) : AbstractCommandValueParameter<String>() {
public override val type: KType get() = STRING_TYPE
public override val isOptional: Boolean get() = false
public override val isVararg: Boolean get() = false
init {
require(expectingValue.isNotBlank()) {
"expectingValue must not be blank"
}
require(expectingValue.none(Char::isWhitespace)) {
"expectingValue must not contain whitespace"
}
}
override fun toString(): String = "<$expectingValue>"
private companion object {
@OptIn(ExperimentalStdlibApi::class)
val STRING_TYPE = typeOf<String>()
}
}
/**
* @see createOptional
* @see createRequired
*/
public class UserDefinedType<T>(
public override val name: String?,
public override val isOptional: Boolean,
public override val isVararg: Boolean,
public override val type: KType,
) : AbstractCommandValueParameter<T>() {
init {
requireNotNull(type.classifierAsKClassOrNull()) {
"type.classifier must be KClass."
}
if (isVararg)
check(type.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) {
"type must be subtype of Array if vararg. Given $type."
}
}
public companion object {
@JvmStatic
public inline fun <reified T : Any> createOptional(name: String, isVararg: Boolean): UserDefinedType<T> {
@OptIn(ExperimentalStdlibApi::class)
return UserDefinedType(name, true, isVararg, typeOf<T>())
}
@JvmStatic
public inline fun <reified T : Any> createRequired(name: String, isVararg: Boolean): UserDefinedType<T> {
@OptIn(ExperimentalStdlibApi::class)
return UserDefinedType(name, false, isVararg, typeOf<T>())
}
}
}
/**
* Extended by [CommandValueArgumentParser]
*/
@ConsoleExperimentalApi
public abstract class Extended<T> : AbstractCommandValueParameter<T>() {
abstract override fun toString(): String
}
}

View File

@ -9,7 +9,7 @@
@file:Suppress("NOTHING_TO_INLINE", "unused")
package net.mamoe.mirai.console.command.description
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.CommandManager
@ -32,27 +32,27 @@ import kotlin.contracts.contract
* ```
* suspend fun CommandSender.mute(target: Member, duration: Int)
* ```
* [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] [Member] [CommandArgumentParser], 并调用其 [CommandArgumentParser.parse]
* [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] [Member] [CommandValueArgumentParser], 并调用其 [CommandValueArgumentParser.parse]
*
* ### 内建指令解析器
* - 基础类型: [ByteArgumentParser], [ShortArgumentParser], [IntArgumentParser], [LongArgumentParser]
* [FloatArgumentParser], [DoubleArgumentParser],
* [BooleanArgumentParser], [StringArgumentParser]
* - 基础类型: [ByteValueArgumentParser], [ShortValueArgumentParser], [IntValueArgumentParser], [LongValueArgumentParser]
* [FloatValueArgumentParser], [DoubleValueArgumentParser],
* [BooleanValueArgumentParser], [StringValueArgumentParser]
*
* - [Bot]: [ExistingBotArgumentParser]
* - [Friend]: [ExistingFriendArgumentParser]
* - [Group]: [ExistingGroupArgumentParser]
* - [Member]: [ExistingMemberArgumentParser]
* - [User]: [ExistingUserArgumentParser]
* - [Contact]: [ExistingContactArgumentParser]
* - [Bot]: [ExistingBotValueArgumentParser]
* - [Friend]: [ExistingFriendValueArgumentParser]
* - [Group]: [ExistingGroupValueArgumentParser]
* - [Member]: [ExistingMemberValueArgumentParser]
* - [User]: [ExistingUserValueArgumentParser]
* - [Contact]: [ExistingContactValueArgumentParser]
*
*
* @see SimpleCommand 简单指令
* @see CompositeCommand 复合指令
*
* @see buildCommandArgumentContext 指令参数环境, [CommandArgumentParser] 的集合
* @see buildCommandArgumentContext 指令参数环境, [CommandValueArgumentParser] 的集合
*/
public interface CommandArgumentParser<out T : Any> {
public interface CommandValueArgumentParser<out T : Any> {
/**
* 解析一个字符串为 [T] 类型参数
*
@ -83,14 +83,14 @@ public interface CommandArgumentParser<out T : Any> {
/**
* 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型.
*/
public fun <T : Any, R : Any> CommandArgumentParser<T>.map(
mapper: CommandArgumentParser<R>.(T) -> R
): CommandArgumentParser<R> = MappingCommandArgumentParser(this, mapper)
public fun <T : Any, R : Any> CommandValueArgumentParser<T>.map(
mapper: CommandValueArgumentParser<R>.(T) -> R,
): CommandValueArgumentParser<R> = MappingCommandValueArgumentParser(this, mapper)
private class MappingCommandArgumentParser<T : Any, R : Any>(
private val original: CommandArgumentParser<T>,
private val mapper: CommandArgumentParser<R>.(T) -> R
) : CommandArgumentParser<R> {
private class MappingCommandValueArgumentParser<T : Any, R : Any>(
private val original: CommandValueArgumentParser<T>,
private val mapper: CommandValueArgumentParser<R>.(T) -> R,
) : CommandValueArgumentParser<R> {
override fun parse(raw: String, sender: CommandSender): R = mapper(original.parse(raw, sender))
override fun parse(raw: MessageContent, sender: CommandSender): R = mapper(original.parse(raw, sender))
}
@ -102,14 +102,14 @@ private class MappingCommandArgumentParser<T : Any, R : Any>(
*/
@JvmSynthetic
@Throws(IllegalArgumentException::class)
public fun <T : Any> CommandArgumentParser<T>.parse(raw: Any, sender: CommandSender): T {
public fun <T : Any> CommandValueArgumentParser<T>.parse(raw: Any, sender: CommandSender): T {
contract {
returns() implies (raw is String || raw is SingleMessage)
}
return when (raw) {
is String -> parse(raw, sender)
is SingleMessage -> parse(raw, sender)
is MessageContent -> parse(raw, sender)
else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}")
}
}
@ -122,7 +122,7 @@ public fun <T : Any> CommandArgumentParser<T>.parse(raw: Any, sender: CommandSen
@Suppress("unused")
@JvmSynthetic
@Throws(CommandArgumentParserException::class)
public inline fun CommandArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing {
public inline fun CommandValueArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing {
throw CommandArgumentParserException(message, cause)
}
@ -133,9 +133,9 @@ public inline fun CommandArgumentParser<*>.illegalArgument(message: String, caus
*/
@Throws(CommandArgumentParserException::class)
@JvmSynthetic
public inline fun CommandArgumentParser<*>.checkArgument(
public inline fun CommandValueArgumentParser<*>.checkArgument(
condition: Boolean,
crossinline message: () -> String = { "Check failed." }
crossinline message: () -> String = { "Check failed." },
) {
contract {
returns() implies condition

View File

@ -0,0 +1,40 @@
/*
* 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("MemberVisibilityCanBePrivate", "unused")
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
import kotlin.reflect.KType
internal val KType.qualifiedName: String
get() = this.classifierAsKClassOrNull()?.qualifiedNameOrTip ?: classifier.toString()
@ExperimentalCommandDescriptors
public open class NoValueArgumentMappingException(
public val argument: CommandValueArgument,
public val forType: KType,
) : CommandResolutionException("Cannot find a CommandArgument mapping for ${forType.qualifiedName}")
@ExperimentalCommandDescriptors
public open class UnresolvedCommandCallException(
public val call: CommandCall,
) : CommandResolutionException("Unresolved call: $call")
public open class CommandResolutionException : RuntimeException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause)
}

View File

@ -0,0 +1,31 @@
/*
* 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.descriptor
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.annotation.AnnotationTarget.*
/**
* 标记一个实验性的指令解释器 API.
*
* 这些 API 不具有稳定性, 且可能会在任意时刻更改.
* 不建议在发行版本中使用这些 API.
*
* @since 1.0-RC
*/
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
@MustBeDocumented
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public annotation class ExperimentalCommandDescriptors(
val message: String = "Command descriptors are an experimental API.",
)

View File

@ -0,0 +1,70 @@
/*
* 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.descriptor
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandCallParser
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.internal.data.castOrNull
import net.mamoe.mirai.console.internal.data.kClassQualifiedName
import net.mamoe.mirai.message.data.*
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* Implicit type variant specified by [CommandCallParser].
*
* [TypeVariant] is not necessary for all [CommandCall]s.
*/
@ExperimentalCommandDescriptors
public interface TypeVariant<out OutType> {
/**
* The reified type of [OutType]
*/
public val outType: KType
/**
* @see CommandValueArgument.value
*/
public fun mapValue(valueParameter: Message): OutType
public companion object {
@OptIn(ExperimentalStdlibApi::class)
@JvmSynthetic
public inline operator fun <reified OutType> invoke(crossinline block: (valueParameter: Message) -> OutType): TypeVariant<OutType> {
return object : TypeVariant<OutType> {
override val outType: KType = typeOf<OutType>()
override fun mapValue(valueParameter: Message): OutType = block(valueParameter)
}
}
}
}
@ExperimentalCommandDescriptors
public object MessageContentTypeVariant : TypeVariant<MessageContent> {
@OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<MessageContent>()
override fun mapValue(valueParameter: Message): MessageContent =
valueParameter.castOrNull<MessageContent>() ?: error("Accepts MessageContent only but given ${valueParameter.kClassQualifiedName}")
}
@ExperimentalCommandDescriptors
public object MessageChainTypeVariant : TypeVariant<MessageChain> {
@OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<MessageChain>()
override fun mapValue(valueParameter: Message): MessageChain = valueParameter.asMessageChain()
}
@ExperimentalCommandDescriptors
public object ContentStringTypeVariant : TypeVariant<String> {
@OptIn(ExperimentalStdlibApi::class)
override val outType: KType = typeOf<String>()
override fun mapValue(valueParameter: Message): String = valueParameter.content
}

View File

@ -9,13 +9,8 @@
package net.mamoe.mirai.console.command.java
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* Java 用户添加协程帮助的 [Command].
@ -24,17 +19,7 @@ import net.mamoe.mirai.message.data.MessageChain
*
* @see Command
*/
@ConsoleExperimentalApi("Not yet supported")
public interface JCommand : Command {
public override suspend fun CommandSender.onCommand(args: MessageChain) {
withContext(Dispatchers.IO) { onCommand(this@onCommand, args) }
}
/**
* 在指令被执行时调用.
*
* @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数.
*
* @see CommandManager.executeCommand 查看更多信息
*/
public fun onCommand(sender: CommandSender, args: MessageChain) // overrides blocking bridge
// TODO: 2020/10/18 JCommand
}

View File

@ -13,10 +13,11 @@ import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.description.buildCommandArgumentContext
import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* 复合指令. 指令注册时候会通过反射构造指令解析器.
@ -68,6 +69,7 @@ import net.mamoe.mirai.console.permission.Permission
*
* @see buildCommandArgumentContext
*/
@ConsoleExperimentalApi("Not yet supported")
public abstract class JCompositeCommand
@JvmOverloads constructor(
owner: CommandOwner,

View File

@ -9,16 +9,15 @@
package net.mamoe.mirai.console.command.java
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* Java 用户继承
@ -46,6 +45,7 @@ import net.mamoe.mirai.message.data.SingleMessage
*
* @see JRawCommand
*/
@ConsoleExperimentalApi("Not yet supported")
public abstract class JRawCommand
@JvmOverloads constructor(
/**
@ -72,19 +72,4 @@ public abstract class JRawCommand
/** 为 `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: MessageChain)
public final override suspend fun CommandSender.onCommand(args: MessageChain) {
withContext(Dispatchers.IO) { onCommand(this@onCommand, args) }
}
}

View File

@ -13,10 +13,11 @@ import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.console.command.description.CommandArgumentContext
import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
/**
* Java 实现:
@ -42,6 +43,7 @@ import net.mamoe.mirai.console.permission.Permission
* @see SimpleCommand
* @see [CommandManager.executeCommand]
*/
@ConsoleExperimentalApi("Not yet supported")
public abstract class JSimpleCommand(
owner: CommandOwner,
@ResolveContext(COMMAND_NAME) primaryName: String,

View File

@ -0,0 +1,41 @@
/*
* 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:OptIn(ExperimentalStdlibApi::class)
package net.mamoe.mirai.console.command.parse
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
/**
* Unresolved [CommandCall].
*/
@ExperimentalCommandDescriptors
public interface CommandCall {
public val caller: CommandSender
/**
* One of callee [Command]'s [Command.allNames]
*/
public val calleeName: String
/**
* Explicit value arguments
*/
public val valueArguments: List<CommandValueArgument>
}
@ExperimentalCommandDescriptors
public class CommandCallImpl(
override val caller: CommandSender,
override val calleeName: String,
override val valueArguments: List<CommandValueArgument>,
) : CommandCall

View File

@ -0,0 +1,46 @@
package net.mamoe.mirai.console.command.parse
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.resolve.CommandCallResolver
import net.mamoe.mirai.console.command.resolve.ResolvedCommandCall
import net.mamoe.mirai.console.extensions.CommandCallParserProvider
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
/**
* Lexical and syntactical parser for transforming a [MessageChain] into [CommandCall]
*
* @see CommandCallResolver The call resolver for [CommandCall] to become [ResolvedCommandCall]
* @see CommandCallParserProvider The extension point
*
* @see SpaceSeparatedCommandCallParser
*/
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public interface CommandCallParser {
/**
* Lexically and syntactically parse a [message] into [CommandCall], but performs nothing about resolving a call.
*
* @return `null` if unable to parse (i.e. due to syntax errors).
*/
public fun parse(caller: CommandSender, message: MessageChain): CommandCall?
public companion object {
/**
* Calls [CommandCallParser]s provided by [CommandCallParserProvider] in [GlobalComponentStorage] sequentially,
* returning the first non-null result, `null` otherwise.
*/
@JvmStatic
public fun MessageChain.parseCommandCall(sender: CommandSender): CommandCall? {
GlobalComponentStorage.run {
CommandCallParserProvider.useExtensions { provider ->
provider.instance.parse(sender, this@parseCommandCall)?.let { return it }
}
}
return null
}
}
}

View File

@ -0,0 +1,132 @@
/*
* 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.parse
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.internal.data.castOrInternalError
import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf
/**
* @see CommandValueArgument
*/
@ExperimentalCommandDescriptors
public interface CommandArgument
/**
* @see DefaultCommandValueArgument
*/
@ExperimentalCommandDescriptors
public interface CommandValueArgument : CommandArgument {
public val type: KType
/**
* [MessageContent] if single argument
* [MessageChain] is vararg
*/
public val value: Message
public val typeVariants: List<TypeVariant<*>>
}
/**
* The [CommandValueArgument] that doesn't vary in type (remaining [MessageContent]).
*/
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public data class DefaultCommandValueArgument(
public override val value: Message,
) : CommandValueArgument {
@OptIn(ExperimentalStdlibApi::class)
override val type: KType = typeOf<MessageContent>()
override val typeVariants: List<TypeVariant<*>> = listOf(
MessageContentTypeVariant,
MessageChainTypeVariant,
ContentStringTypeVariant,
)
}
@ExperimentalCommandDescriptors
public fun <T> CommandValueArgument.mapValue(typeVariant: TypeVariant<T>): T = typeVariant.mapValue(this.value)
@OptIn(ExperimentalStdlibApi::class)
@ExperimentalCommandDescriptors
public inline fun <reified T> CommandValueArgument.mapToType(): T =
mapToTypeOrNull() ?: throw NoValueArgumentMappingException(this, typeOf<T>())
@OptIn(ExperimentalStdlibApi::class)
@ExperimentalCommandDescriptors
public fun <T> CommandValueArgument.mapToType(type: KType): T =
mapToTypeOrNull(type) ?: throw NoValueArgumentMappingException(this, type)
@ExperimentalCommandDescriptors
public fun <T> CommandValueArgument.mapToTypeOrNull(expectingType: KType): T? {
if (expectingType.isSubtypeOf(ARRAY_OUT_ANY_TYPE)) {
val arrayElementType = expectingType.arguments.single().type ?: ANY_TYPE
val result = ArrayList<Any?>()
when (val value = value) {
is MessageChain -> {
for (message in value) {
result.add(mapToTypeOrNullImpl(arrayElementType, message))
}
}
else -> { // single
value.castOrInternalError<SingleMessage>()
result.add(mapToTypeOrNullImpl(arrayElementType, value))
}
}
@Suppress("UNCHECKED_CAST")
return result.toArray(arrayElementType.createArray(result.size)) as T
}
@Suppress("UNCHECKED_CAST")
return mapToTypeOrNullImpl(expectingType, value) as T
}
private fun KType.createArray(size: Int): Array<Any?> {
return java.lang.reflect.Array.newInstance(this.classifierAsKClass().javaObjectType, size).castOrInternalError()
}
@OptIn(ExperimentalCommandDescriptors::class)
private fun CommandValueArgument.mapToTypeOrNullImpl(expectingType: KType, value: Message): Any? {
@OptIn(ExperimentalStdlibApi::class)
val result = typeVariants
.filter { it.outType.isSubtypeOf(expectingType) }
.ifEmpty {
return null
}
.reduce { acc, typeVariant ->
if (acc.outType.isSubtypeOf(typeVariant.outType))
acc
else typeVariant
}
@Suppress("UNCHECKED_CAST")
return result.mapValue(value)
}
@ExperimentalCommandDescriptors
public inline fun <reified T> CommandValueArgument.mapToTypeOrNull(): T? {
@OptIn(ExperimentalStdlibApi::class)
return mapToTypeOrNull(typeOf<T>())
}

View File

@ -0,0 +1,26 @@
package net.mamoe.mirai.console.command.parse
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.extensions.CommandCallParserProvider
import net.mamoe.mirai.console.internal.command.flattenCommandComponents
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.content
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public object SpaceSeparatedCommandCallParser : CommandCallParser {
override fun parse(caller: CommandSender, message: MessageChain): CommandCall? {
val flatten = message.flattenCommandComponents().filterIsInstance<MessageContent>()
if (flatten.isEmpty()) return null
return CommandCallImpl(
caller = caller,
calleeName = flatten.first().content,
valueArguments = flatten.drop(1).map(::DefaultCommandValueArgument)
)
}
public object Provider : CommandCallParserProvider(SpaceSeparatedCommandCallParser)
}

View File

@ -0,0 +1,172 @@
package net.mamoe.mirai.console.command.resolve
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.descriptor.ArgumentAcceptance.Companion.isNotAcceptable
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.command.parse.DefaultCommandValueArgument
import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.safeCast
import net.mamoe.mirai.message.data.EmptyMessageChain
import net.mamoe.mirai.message.data.asMessageChain
/**
* Builtin implementation of [CommandCallResolver]
*/
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public object BuiltInCommandCallResolver : CommandCallResolver {
public object Provider : CommandCallResolverProvider(BuiltInCommandCallResolver)
override fun resolve(call: CommandCall): ResolvedCommandCall? {
val callee = CommandManager.matchCommand(call.calleeName) ?: return null
val valueArguments = call.valueArguments
val context = callee.safeCast<CommandArgumentContextAware>()?.context
val signature = resolveImpl(callee, valueArguments, context) ?: return null
return ResolvedCommandCallImpl(call.caller,
callee,
signature.variant,
signature.zippedArguments.map { it.second },
context ?: EmptyCommandArgumentContext)
}
private data class ResolveData(
val variant: CommandSignatureVariant,
val zippedArguments: List<Pair<AbstractCommandValueParameter<*>, CommandValueArgument>>,
val argumentAcceptances: List<ArgumentAcceptanceWithIndex>,
val remainingParameters: List<AbstractCommandValueParameter<*>>,
) {
val remainingOptionalCount: Int = remainingParameters.count { it.isOptional }
}
private data class ArgumentAcceptanceWithIndex(
val index: Int,
val acceptance: ArgumentAcceptance,
)
private fun resolveImpl(
callee: Command,
valueArguments: List<CommandValueArgument>,
context: CommandArgumentContext?,
): ResolveData? {
callee.overloads
.mapNotNull l@{ signature ->
val valueParameters = signature.valueParameters
val zipped = valueParameters.zip(valueArguments).toMutableList()
val remainingParameters = valueParameters.drop(zipped.size).toMutableList()
if (remainingParameters.any { !it.isOptional && !it.isVararg }) return@l null // not enough args. // vararg can be empty.
if (zipped.isEmpty()) {
ResolveData(
variant = signature,
zippedArguments = emptyList(),
argumentAcceptances = emptyList(),
remainingParameters = remainingParameters,
)
} else {
if (valueArguments.size > valueParameters.size && zipped.last().first.isVararg) {
// merge vararg arguments
val (varargParameter, varargFirstArgument)
= zipped.removeLast()
zipped.add(varargParameter to DefaultCommandValueArgument(valueArguments.drop(zipped.size).map { it.value }.asMessageChain()))
} else {
// add default empty vararg argument
val remainingVararg = remainingParameters.find { it.isVararg }
if (remainingVararg != null) {
zipped.add(remainingVararg to DefaultCommandValueArgument(EmptyMessageChain))
remainingParameters.remove(remainingVararg)
}
}
ResolveData(
variant = signature,
zippedArguments = zipped,
argumentAcceptances = zipped.mapIndexed { index, (parameter, argument) ->
val accepting = parameter.accepting(argument, context)
if (accepting.isNotAcceptable) {
return@l null // argument type not assignable
}
ArgumentAcceptanceWithIndex(index, accepting)
},
remainingParameters = remainingParameters
)
}
}
.also { result -> result.singleOrNull()?.let { return it } }
.takeLongestMatches()
.ifEmpty { return null }
.also { result -> result.singleOrNull()?.let { return it } }
// take single ArgumentAcceptance.Direct
.also { list ->
val candidates = list
.flatMap { phase ->
phase.argumentAcceptances.filter { it.acceptance is ArgumentAcceptance.Direct }.map { phase to it }
}
candidates.singleOrNull()?.let { return it.first } // single Direct
if (candidates.distinctBy { it.second.index }.size != candidates.size) {
// Resolution ambiguity
/*
open class A
open class AA: A()
open class C
open class CC: C()
fun foo(a: A, c: CC) = 1
fun foo(a: AA, c: C) = 1
*/
// The call is foo(AA(), C()) or foo(A(), CC())
return null
}
}
return null
}
/*
open class A
open class B : A()
open class C : A()
open class D : C()
open class BB : B()
fun foo(a: A, c: C) = 1
//fun foo(a: A, c: A) = 1
//fun foo(a: A, c: C, def: Int = 0) = 1
fun foo(a: B, c: C, d: D) = ""
fun foo(b: BB, a: A, d: C) = 1.0
fun main() {
val a = foo(D(), D()) // int
val b = foo(A(), C()) // int
val d = foo(BB(), c = C(), D()) // string
}
*/
private fun List<ResolveData>.takeLongestMatches(): Collection<ResolveData> {
if (isEmpty()) return emptyList()
return associateWith {
it.variant.valueParameters.size - it.remainingOptionalCount * 1.001 // slightly lower priority with optional defaults.
}.let { m ->
val maxMatch = m.values.maxByOrNull { it }
m.filter { it.value == maxMatch }.keys
}
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.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 []
*
* @see CommandCallResolverProvider The provider to instances of this class
* @see BuiltInCommandCallResolver The builtin implementation
*/
@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

@ -0,0 +1,87 @@
/*
* 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.resolve
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.parse.CommandCall
import net.mamoe.mirai.console.command.parse.CommandValueArgument
import net.mamoe.mirai.console.command.parse.mapToTypeOrNull
import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.cast
import kotlin.LazyThreadSafetyMode.PUBLICATION
/**
* The resolved [CommandCall].
*
* @see ResolvedCommandCallImpl
*/
@ExperimentalCommandDescriptors
public interface ResolvedCommandCall {
public val caller: CommandSender
/**
* The callee [Command]
*/
public val callee: Command
/**
* The callee [CommandSignatureVariant], specifically a sub command from [CompositeCommand]
*/
public val calleeSignature: CommandSignatureVariant
/**
* Original arguments
*/
public val rawValueArguments: List<CommandValueArgument>
/**
* Resolved value arguments arranged mapping the [CommandSignatureVariant.valueParameters] by index.
*
* **Implementation details**: Lazy calculation.
*/
@ConsoleExperimentalApi
public val resolvedValueArguments: List<ResolvedCommandValueArgument<*>>
public companion object
}
@ExperimentalCommandDescriptors
public data class ResolvedCommandValueArgument<T>(
val parameter: CommandValueParameter<T>,
val value: T,
)
// Don't move into companion, compilation error
@ExperimentalCommandDescriptors
public suspend inline fun ResolvedCommandCall.call() {
return this@call.calleeSignature.call(this@call)
}
@ExperimentalCommandDescriptors
public class ResolvedCommandCallImpl(
override val caller: CommandSender,
override val callee: Command,
override val calleeSignature: CommandSignatureVariant,
override val rawValueArguments: List<CommandValueArgument>,
private val context: CommandArgumentContext,
) : ResolvedCommandCall {
override val resolvedValueArguments: List<ResolvedCommandValueArgument<*>> by lazy(PUBLICATION) {
calleeSignature.valueParameters.zip(rawValueArguments).map { (parameter, argument) ->
val value = argument.mapToTypeOrNull(parameter.type) ?: context[parameter.type.classifierAsKClass()]?.parse(argument.value, caller)
?: throw NoValueArgumentMappingException(argument, parameter.type)
// TODO: 2020/10/17 consider vararg and optional
ResolvedCommandValueArgument(parameter.cast(), value)
}
}
}

View File

@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.plugin.updateWhen
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.*

View File

@ -24,6 +24,12 @@ public open class AbstractExtensionPoint<T : Extension>(
public override val extensionType: KClass<T>,
) : ExtensionPoint<T>
public open class InstanceExtensionPoint<E : InstanceExtension<T>, T>(
extensionType: KClass<E>,
public vararg val builtinImplementations: E,
) : AbstractExtensionPoint<E>(extensionType)
/**
* 表示一个 [SingletonExtension] [ExtensionPoint]
*/

View File

@ -0,0 +1,25 @@
/*
* 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.extensions
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCallParser
import net.mamoe.mirai.console.command.parse.SpaceSeparatedCommandCallParser
import net.mamoe.mirai.console.extension.InstanceExtension
import net.mamoe.mirai.console.extension.InstanceExtensionPoint
/**
* The provider of [CommandCallParser]
*/
@ExperimentalCommandDescriptors
public open class CommandCallParserProvider(override val instance: CommandCallParser) : InstanceExtension<CommandCallParser> {
public companion object ExtensionPoint :
InstanceExtensionPoint<CommandCallParserProvider, CommandCallParser>(CommandCallParserProvider::class, SpaceSeparatedCommandCallParser.Provider)
}

View File

@ -0,0 +1,22 @@
/*
* 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.extensions
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.resolve.BuiltInCommandCallResolver
import net.mamoe.mirai.console.command.resolve.CommandCallResolver
import net.mamoe.mirai.console.extension.InstanceExtension
import net.mamoe.mirai.console.extension.InstanceExtensionPoint
@ExperimentalCommandDescriptors
public open class CommandCallResolverProvider(override val instance: CommandCallResolver) : InstanceExtension<CommandCallResolver> {
public companion object ExtensionPoint :
InstanceExtensionPoint<CommandCallResolverProvider, CommandCallResolver>(CommandCallResolverProvider::class, BuiltInCommandCallResolver.Provider)
}

View File

@ -29,7 +29,7 @@ import kotlin.reflect.KClass
*/
public interface SingletonExtensionSelector : FunctionExtension {
public data class Registry<T : Extension>(
val plugin: Plugin,
val plugin: Plugin?,
val extension: T,
)
@ -55,11 +55,11 @@ public interface SingletonExtensionSelector : FunctionExtension {
instances.isEmpty() -> BuiltInSingletonExtensionSelector
instances.size == 1 -> {
instances.single().also { (plugin, ext) ->
MiraiConsole.mainLogger.info { "Loaded SingletonExtensionSelector: $ext from ${plugin.name}" }
MiraiConsole.mainLogger.info { "Loaded SingletonExtensionSelector: $ext from ${plugin?.name ?: "<builtin>"}" }
}.extension
}
else -> {
error("Found too many SingletonExtensionSelectors: ${instances.joinToString { (p, i) -> "'$i' from '${p.name}'" }}. Check your plugins and ensure there is only one external SingletonExtensionSelectors")
error("Found too many SingletonExtensionSelectors: ${instances.joinToString { (p, i) -> "'$i' from '${p?.name ?: "<builtin>"}'" }}. Check your plugins and ensure there is only one external SingletonExtensionSelectors")
}
}
}

View File

@ -15,18 +15,25 @@ 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.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
internal object CommandManagerImpl : CommandManager, CoroutineScope by CoroutineScope(MiraiConsole.job) {
@OptIn(ExperimentalCommandDescriptors::class)
internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiConsole.childScope("CommandManagerImpl") {
private val logger: MiraiLogger by lazy {
MiraiConsole.createLogger("command")
}
@ -48,11 +55,11 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
/**
* 从原始的 command 中解析出 Command 对象
*/
internal fun matchCommand(rawCommand: String): Command? {
if (rawCommand.startsWith(commandPrefix)) {
return requiredPrefixCommandMap[rawCommand.substringAfter(commandPrefix).toLowerCase()]
override fun matchCommand(commandName: String): Command? {
if (commandName.startsWith(commandPrefix)) {
return requiredPrefixCommandMap[commandName.substringAfter(commandPrefix).toLowerCase()]
}
return optionalPrefixCommandMap[rawCommand.toLowerCase()]
return optionalPrefixCommandMap[commandName.toLowerCase()]
}
internal val commandListener: Listener<MessageEvent> by lazy {
@ -65,13 +72,17 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
) {
val sender = this.toCommandSender()
when (val result = sender.executeCommand(message)) {
when (val result = executeCommand(sender, message)) {
is CommandExecuteResult.PermissionDenied -> {
if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) {
sender.sendMessage("权限不足")
intercept()
}
}
is CommandExecuteResult.IllegalArgument -> {
result.exception.message?.let { sender.sendMessage(it) }
intercept()
}
is CommandExecuteResult.Success -> {
intercept()
}
@ -79,7 +90,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
sender.catchExecutionException(result.exception)
intercept()
}
is CommandExecuteResult.CommandNotFound -> {
is CommandExecuteResult.UnresolvedCall -> {
// noop
}
}
@ -90,102 +101,90 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
///// 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.subCommands // 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 {
optionalPrefixCommandMap.remove(it)
override fun unregisterCommand(command: Command): Boolean = modifyLock.withLock {
if (command.prefixOptional) {
command.allNames.forEach {
optionalPrefixCommandMap.remove(it.toLowerCase())
}
}
this.allNames.forEach {
requiredPrefixCommandMap.remove(it)
command.allNames.forEach {
requiredPrefixCommandMap.remove(it.toLowerCase())
}
_registeredCommands.remove(this)
_registeredCommands.remove(command)
}
override fun Command.isRegistered(): Boolean = this in _registeredCommands
override suspend fun Command.execute(
sender: CommandSender,
arguments: Message,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
arguments.flattenCommandComponents(),
primaryName,
checkPermission
)
override fun isCommandRegistered(command: Command): Boolean = command in _registeredCommands
}
override suspend fun Command.execute(
sender: CommandSender,
arguments: String,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
arguments.flattenCommandComponents(),
primaryName,
checkPermission
)
}
override suspend fun CommandSender.executeCommand(
// Don't move into CommandManager, compilation error / VerifyError
@OptIn(ExperimentalCommandDescriptors::class)
internal suspend fun executeCommandImpl(
message: Message,
checkPermission: Boolean
caller: CommandSender,
checkPermission: Boolean,
): CommandExecuteResult {
val msg = message.asMessageChain().filterIsInstance<MessageContent>()
if (msg.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(msg, msg[0].content.substringBefore(' '), checkPermission)
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)
}
override suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean): CommandExecuteResult {
if (message.isBlank()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(message, message.substringBefore(' '), checkPermission)
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

@ -0,0 +1,242 @@
package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.internal.data.classifierAsKClass
import net.mamoe.mirai.console.internal.data.classifierAsKClassOrNull
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.buildMessageChain
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KVisibility
import kotlin.reflect.full.*
internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray()
internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain {
when (this@flattenCommandComponents) {
is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is SingleMessage -> add(this@flattenCommandComponents)
is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
else -> add(this@flattenCommandComponents.toString())
}
}
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal object CompositeCommandSubCommandAnnotationResolver :
SubCommandAnnotationResolver {
override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) =
function.hasAnnotation<CompositeCommand.SubCommand>()
override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> {
val annotated = function.findAnnotation<CompositeCommand.SubCommand>()!!.value
return if (annotated.isEmpty()) arrayOf(function.name)
else annotated
}
override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? =
parameter.findAnnotation<CompositeCommand.Name>()?.value
override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? =
function.findAnnotation<CompositeCommand.Description>()?.value
}
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal object SimpleCommandSubCommandAnnotationResolver :
SubCommandAnnotationResolver {
override fun hasAnnotation(ownerCommand: Command, function: KFunction<*>) =
function.hasAnnotation<SimpleCommand.Handler>()
override fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String> =
ownerCommand.secondaryNames
override fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String? =
parameter.findAnnotation<SimpleCommand.Name>()?.value
override fun getDescription(ownerCommand: Command, function: KFunction<*>): String? =
ownerCommand.description
}
internal interface SubCommandAnnotationResolver {
fun hasAnnotation(ownerCommand: Command, function: KFunction<*>): Boolean
fun getSubCommandNames(ownerCommand: Command, function: KFunction<*>): Array<out String>
fun getAnnotatedName(ownerCommand: Command, parameter: KParameter): String?
fun getDescription(ownerCommand: Command, function: KFunction<*>): String?
}
@ConsoleExperimentalApi
public class IllegalCommandDeclarationException : Exception {
public override val message: String?
public constructor(
ownerCommand: Command,
correspondingFunction: KFunction<*>,
message: String?,
) : super("Illegal command declaration: ${correspondingFunction.name} declared in ${ownerCommand::class.qualifiedName}") {
this.message = message
}
public constructor(
ownerCommand: Command,
message: String?,
) : super("Illegal command declaration: ${ownerCommand::class.qualifiedName}") {
this.message = message
}
}
@OptIn(ExperimentalCommandDescriptors::class)
internal class CommandReflector(
val command: Command,
val annotationResolver: SubCommandAnnotationResolver,
) {
@Suppress("NOTHING_TO_INLINE")
private inline fun KFunction<*>.illegalDeclaration(
message: String,
): Nothing {
throw IllegalCommandDeclarationException(command, this, message)
}
private fun KFunction<*>.isSubCommandFunction(): Boolean = annotationResolver.hasAnnotation(command, this)
private fun KFunction<*>.checkExtensionReceiver() {
this.extensionReceiverParameter?.let { receiver ->
if (receiver.type.classifierAsKClassOrNull()?.isSubclassOf(CommandSender::class) != true) {
illegalDeclaration("Extension receiver parameter type is not subclass of CommandSender.")
}
}
}
private fun KFunction<*>.checkNames() {
val names = annotationResolver.getSubCommandNames(command, this)
for (name in names) {
ILLEGAL_SUB_NAME_CHARS.find { it in name }?.let {
illegalDeclaration("'$it' is forbidden in command name.")
}
}
}
private fun KFunction<*>.checkModifiers() {
if (isInline) illegalDeclaration("Command function cannot be inline")
if (visibility == KVisibility.PRIVATE) illegalDeclaration("Command function must be accessible from Mirai Console, that is, effectively public.")
if (this.hasAnnotation<JvmStatic>()) illegalDeclaration("Command function must not be static.")
// should we allow abstract?
// if (isAbstract) illegalDeclaration("Command function cannot be abstract")
}
fun generateUsage(overloads: Iterable<CommandSignatureVariantFromKFunction>): String {
return overloads.joinToString("\n") { subcommand ->
buildString {
if (command.prefixOptional) {
append("(")
append(CommandManager.commandPrefix)
append(")")
} else {
append(CommandManager.commandPrefix)
}
if (command is CompositeCommand) {
append(command.primaryName)
append(" ")
}
append(subcommand.valueParameters.joinToString(" ") { it.render() })
annotationResolver.getDescription(command, subcommand.originFunction).let { description ->
append(" ")
append(description)
}
}
}
}
companion object {
private fun <T> AbstractCommandValueParameter<T>.render(): String {
return when (this) {
is AbstractCommandValueParameter.Extended,
is AbstractCommandValueParameter.UserDefinedType<*>,
-> {
"<${this.name ?: this.type.classifierAsKClass().simpleName}>"
}
is AbstractCommandValueParameter.StringConstant -> {
this.expectingValue
}
}
}
}
fun validate(variants: List<CommandSignatureVariantFromKFunctionImpl>) {
data class ErasedParameters(
val name: String,
val x: String,
)
variants
}
@Throws(IllegalCommandDeclarationException::class)
fun findSubCommands(): List<CommandSignatureVariantFromKFunctionImpl> {
return command::class.functions // exclude static later
.asSequence()
.filter { it.isSubCommandFunction() }
.onEach { it.checkExtensionReceiver() }
.onEach { it.checkModifiers() }
.onEach { it.checkNames() }
.map { function ->
val functionNameAsValueParameter =
annotationResolver.getSubCommandNames(command, function).mapIndexed { index, s -> createStringConstantParameter(index, s) }
val functionValueParameters =
function.valueParameters.associateBy { it.toUserDefinedCommandParameter() }
CommandSignatureVariantFromKFunctionImpl(
receiverParameter = function.extensionReceiverParameter?.toCommandReceiverParameter(),
valueParameters = functionNameAsValueParameter + functionValueParameters.keys,
originFunction = function
) { call ->
val args = LinkedHashMap<KParameter, Any?>()
for ((commandParameter, value) in call.resolvedValueArguments) {
if (commandParameter is AbstractCommandValueParameter.StringConstant) {
continue
}
val functionParameter =
functionValueParameters[commandParameter] ?: error("Could not find a corresponding function parameter '${commandParameter.name}'")
args[functionParameter] = value
}
val instanceParameter = function.instanceParameter
if (instanceParameter != null) {
args[instanceParameter] = command
}
function.callSuspendBy(args)
}
}.toList()
}
private fun KParameter.toCommandReceiverParameter(): CommandReceiverParameter<out CommandSender>? {
check(!this.isVararg) { "Receiver cannot be vararg." }
check(this.type.classifierAsKClass().isSubclassOf(CommandSender::class)) { "Receiver must be subclass of CommandSender" }
return CommandReceiverParameter(this.type.isMarkedNullable, this.type)
}
private fun createStringConstantParameter(index: Int, expectingValue: String): AbstractCommandValueParameter.StringConstant {
return AbstractCommandValueParameter.StringConstant("#$index", expectingValue)
}
private fun KParameter.toUserDefinedCommandParameter(): AbstractCommandValueParameter.UserDefinedType<*> {
return AbstractCommandValueParameter.UserDefinedType<Any?>(nameForCommandParameter(), this.isOptional, this.isVararg, this.type) // Any? is erased
}
private fun KParameter.nameForCommandParameter(): String? = annotationResolver.getAnnotatedName(command, this) ?: this.name
}

View File

@ -12,22 +12,24 @@
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
import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser
import kotlin.reflect.KParameter
import kotlin.reflect.KType
/*
internal fun Parameter.toCommandParam(): CommandParameter<*> {
val name = getAnnotation(CompositeCommand.Name::class.java)
return CommandParameter(
name?.value ?: this.name
?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"),
this.type.kotlin
this.type.kotlin,
null
)
}
*/
/**
* 指令形式参数.
* @see toCommandParam
*/
internal data class CommandParameter<T : Any>(
/**
@ -35,24 +37,27 @@ internal data class CommandParameter<T : Any>(
*/
val name: String,
/**
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgumentParser] 解析.
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandValueArgumentParser] 解析.
*/
val type: KClass<T> // exact type
val type: KType, // exact type
val parameter: KParameter, // source parameter
) {
constructor(name: String, type: KType, parameter: KParameter, parser: CommandValueArgumentParser<T>) : this(
name, type, parameter
) {
constructor(name: String, type: KClass<T>, parser: CommandArgumentParser<T>) : this(name, type) {
this._overrideParser = parser
}
@Suppress("PropertyName")
@JvmField
internal var _overrideParser: CommandArgumentParser<T>? = null
internal var _overrideParser: CommandValueArgumentParser<T>? = null
/**
* 覆盖的 [CommandArgumentParser].
* 覆盖的 [CommandValueArgumentParser].
*
* 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandArgumentParser]
* 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandValueArgumentParser]
*/
val overrideParser: CommandArgumentParser<T>? get() = _overrideParser
val overrideParser: CommandValueArgumentParser<T>? get() = _overrideParser
}

View File

@ -1,349 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
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.internal.data.kClassQualifiedNameOrTip
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission
import net.mamoe.mirai.message.data.*
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
internal object CompositeCommandSubCommandAnnotationResolver :
AbstractReflectionCommand.SubCommandAnnotationResolver {
override fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>) =
function.hasAnnotation<CompositeCommand.SubCommand>()
override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> =
function.findAnnotation<CompositeCommand.SubCommand>()!!.value
}
internal object SimpleCommandSubCommandAnnotationResolver :
AbstractReflectionCommand.SubCommandAnnotationResolver {
override fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>) =
function.hasAnnotation<SimpleCommand.Handler>()
override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> =
baseCommand.secondaryNames
}
internal abstract class AbstractReflectionCommand
@JvmOverloads constructor(
owner: CommandOwner,
primaryName: String,
secondaryNames: Array<out String>,
description: String = "<no description available>",
parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false,
) : Command, AbstractCommand(
owner,
primaryName = primaryName,
secondaryNames = secondaryNames,
description = description,
parentPermission = parentPermission,
prefixOptional = prefixOptional
), CommandArgumentContextAware {
internal abstract val subCommandAnnotationResolver: SubCommandAnnotationResolver
@JvmField
@Suppress("PropertyName")
internal var _usage: String = "<not yet initialized>"
override val usage: String // initialized by subCommand reflection
get() {
subCommands // ensure init
return _usage
}
abstract suspend fun CommandSender.onDefault(rawArgs: MessageChain)
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
DefaultSubCommandDescriptor(
"",
createOrFindCommandPermission(parentPermission),
onCommand = { sender: CommandSender, args: MessageChain ->
sender.onDefault(args)
}
)
}
internal open fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) {
}
interface SubCommandAnnotationResolver {
fun hasAnnotation(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Boolean
fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String>
}
internal val subCommands: Array<SubCommandDescriptor> by lazy {
this::class.declaredFunctions.filter { subCommandAnnotationResolver.hasAnnotation(this, it) }
.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 ->
createSubCommand(function, context)
}.toTypedArray().also {
_usage = it.createUsage(this)
}.also { checkSubCommand(it) }
}
internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> by lazy {
kotlin.run {
val map = LinkedHashMap<Array<String>, SubCommandDescriptor>(subCommands.size * 2)
for (descriptor in subCommands) {
for (name in descriptor.bakedSubNames) {
map[name] = descriptor
}
}
map.toSortedMap { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }
}
}
internal class DefaultSubCommandDescriptor(
val description: String,
val permission: Permission,
val onCommand: suspend (sender: CommandSender, rawArgs: MessageChain) -> Unit,
)
internal inner class SubCommandDescriptor(
val names: Array<out String>,
val params: Array<CommandParameter<*>>,
val description: String,
val permission: Permission,
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean,
val context: CommandArgumentContext,
) {
val usage: String = createUsage(this@AbstractReflectionCommand)
internal suspend fun parseAndExecute(
sender: CommandSender,
argsWithSubCommandNameNotRemoved: MessageChain,
removeSubName: Boolean,
) {
val args = parseArgs(sender, argsWithSubCommandNameNotRemoved, if (removeSubName) 1 else 0)
if (!this.permission.testPermission(sender)) {
sender.sendMessage(usage) // TODO: 2020/8/26 #127
return
}
if (args == null || !onCommand(sender, args)) {
sender.sendMessage(usage)
}
}
@JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: MessageChain, offset: Int): Array<out Any>? {
if (rawArgs.size < offset + this.params.size)
return null
//require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" }
return Array(this.params.size) { index ->
val param = params[index]
val rawArg = rawArgs[offset + index]
when (rawArg) {
is PlainText -> context[param.type]?.parse(rawArg.content, sender)
is MessageContent -> context[param.type]?.parse(rawArg, sender)
else -> throw IllegalArgumentException("Illegal Message kind: ${rawArg.kClassQualifiedNameOrTip}")
} ?: error("Cannot find a parser for $rawArg")
}
}
}
/**
* @param rawArgs 元素类型必须为 [SingleMessage] [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException]
*/
internal fun matchSubCommand(rawArgs: MessageChain): SubCommandDescriptor? {
val maxCount = rawArgs.size
var cur = 0
bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) ->
if (name.size != cur) {
if (cur++ == maxCount) return null
}
if (name.contentEqualsOffset(rawArgs, length = cur)) {
return descriptor
}
}
return null
}
}
internal fun <T> Array<T>.contentEqualsOffset(other: MessageChain, length: Int): Boolean {
repeat(length) { index ->
if (!other[index].toString().equals(this[index].toString(), ignoreCase = true)) {
return false
}
}
return true
}
internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray()
internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this }
internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isBlank() }.toTypedArray()
internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain {
when (this@flattenCommandComponents) {
is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() }
.forEach { +PlainText(it) }
is SingleMessage -> add(this@flattenCommandComponents)
is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
else -> add(this@flattenCommandComponents.toString())
}
}
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
internal fun Array<AbstractReflectionCommand.SubCommandDescriptor>.createUsage(baseCommand: AbstractReflectionCommand): String =
buildString {
appendLine(baseCommand.description)
appendLine()
for (subCommandDescriptor in this@createUsage) {
appendLine(subCommandDescriptor.usage)
}
}.trimEnd()
internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseCommand: AbstractReflectionCommand): String =
buildString {
if (baseCommand.prefixOptional) {
append("(")
append(CommandManager.commandPrefix)
append(")")
} else {
append(CommandManager.commandPrefix)
}
if (baseCommand is CompositeCommand) {
append(baseCommand.primaryName)
append(" ")
}
append(names.first())
append(" ")
append(params.joinToString(" ") { "<${it.name}>" })
append(" ")
append(description)
appendLine()
}.trimEnd()
internal fun AbstractReflectionCommand.createSubCommand(
function: KFunction<*>,
context: CommandArgumentContext,
): AbstractReflectionCommand.SubCommandDescriptor {
val notStatic = !function.hasAnnotation<JvmStatic>()
//val overridePermission = null//function.findAnnotation<CompositeCommand.PermissionId>()//optional
val subDescription =
function.findAnnotation<CompositeCommand.Description>()?.value ?: ""
fun KClass<*>.isValidReturnType(): Boolean {
return when (this) {
Boolean::class, Void::class, Unit::class, Nothing::class -> true
else -> false
}
}
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 =
subCommandAnnotationResolver.getSubCommandNames(this, function)
.let { namesFromAnnotation ->
if (namesFromAnnotation.isNotEmpty()) {
namesFromAnnotation.map(String::toLowerCase).toTypedArray()
} else arrayOf(function.name.toLowerCase())
}.also { names ->
names.forEach {
check(it.isValidSubName()) {
"Name of sub command ${function.name} is invalid"
}
}
}
//map parameter
val params = parameters.map { param ->
if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
val paramName = param.findAnnotation<CompositeCommand.Name>()?.value ?: param.name ?: "unknown"
CommandParameter(
paramName,
(param.type.classifier as? KClass<*>)
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)")
)
}.toTypedArray()
return SubCommandDescriptor(
commandName,
params,
subDescription, // overridePermission?.value
permission,//overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission,
onCommand = { 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.
},
context = context
)
}

View File

@ -1,62 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.asMessageChain
@JvmSynthetic
@Throws(CommandExecutionException::class)
internal suspend fun CommandSender.executeCommandInternal(
command: Command,
args: MessageChain,
commandName: String,
checkPermission: Boolean,
): CommandExecuteResult {
if (checkPermission && !command.permission.testPermission(this)) {
return CommandExecuteResult.PermissionDenied(command, commandName)
}
kotlin.runCatching {
command.onCommand(this, args)
}.fold(
onSuccess = {
return CommandExecuteResult.Success(
commandName = commandName,
command = command,
args = args
)
},
onFailure = {
return CommandExecuteResult.ExecutionFailed(
commandName = commandName,
command = command,
exception = it,
args = args
)
}
)
}
@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()
return executeCommandInternal(command, args.drop(1).asMessageChain(), commandName, checkPermission)
}

View File

@ -12,7 +12,6 @@ package net.mamoe.mirai.console.internal.data
import kotlinx.serialization.json.Json
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SilentLogger

View File

@ -11,14 +11,15 @@ package net.mamoe.mirai.console.internal.data
import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.data.ValueName
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.*
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
@Suppress("UNCHECKED_CAST")
internal inline fun <reified T : Any> KType.toKClass(): KClass<out T> {
val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" }
@ -54,6 +55,12 @@ internal fun KType.classifierAsKClass() = when (val t = classifier) {
else -> error("Only KClass supported as classifier, got $t")
} as KClass<Any>
@Suppress("UNCHECKED_CAST")
internal fun KType.classifierAsKClassOrNull() = when (val t = classifier) {
is KClass<*> -> t
else -> null
} as KClass<Any>?
@JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }

View File

@ -16,7 +16,6 @@ import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.data.SerializableValue.Companion.serializableValueWith
import net.mamoe.mirai.console.data.SerializerAwareValue
import net.mamoe.mirai.console.data.valueFromKType
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import kotlin.contracts.contract
import kotlin.reflect.KClass
import kotlin.reflect.KType

View File

@ -52,7 +52,7 @@ internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector {
val candidatesList = candidates.toList()
for ((index, candidate) in candidatesList.withIndex()) {
MiraiConsole.mainLogger.info { "${index + 1}. '${candidate.extension}' from '${candidate.plugin.name}'" }
MiraiConsole.mainLogger.info { "${index + 1}. '${candidate.extension}' from '${candidate.plugin?.name ?: "<builtin>"}'" }
}
MiraiConsole.mainLogger.info { "Please choose a number from 1 to ${candidatesList.count()}" }

View File

@ -20,12 +20,15 @@ import java.util.concurrent.CopyOnWriteArraySet
import kotlin.contracts.contract
import kotlin.reflect.KClass
/**
* The [ComponentStorage] containing all components provided by Mirai Console internals and installed plugins.
*/
internal object GlobalComponentStorage : AbstractConcurrentComponentStorage()
internal interface ExtensionRegistry<out E : Extension> {
val plugin: Plugin
val plugin: Plugin?
val extension: E
operator fun component1(): Plugin {
operator fun component1(): Plugin? {
return this.plugin
}
@ -35,21 +38,27 @@ internal interface ExtensionRegistry<out E : Extension> {
}
internal class LazyExtensionRegistry<out E : Extension>(
override val plugin: Plugin,
override val plugin: Plugin?,
initializer: () -> E,
) : ExtensionRegistry<E> {
override val extension: E by lazy { initializer() }
}
internal data class DataExtensionRegistry<out E : Extension>(
override val plugin: Plugin,
override val plugin: Plugin?,
override val extension: E,
) : ExtensionRegistry<E>
internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("UNCHECKED_CAST")
internal fun <T : Extension> ExtensionPoint<out T>.getExtensions(): Set<ExtensionRegistry<T>> {
return instances.getOrPut(this, ::CopyOnWriteArraySet) as Set<ExtensionRegistry<T>>
val userDefined = instances.getOrPut(this, ::CopyOnWriteArraySet) as Set<ExtensionRegistry<T>>
val builtins = if (this is InstanceExtensionPoint<*, *>) {
this.builtinImplementations.mapTo(HashSet()) { DataExtensionRegistry(null, it) } as Set<ExtensionRegistry<T>>
} else null
return builtins?.plus(userDefined) ?: userDefined
}
internal fun mergeWith(another: AbstractConcurrentComponentStorage) {
@ -68,7 +77,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
internal inline fun <T : Extension> ExtensionPoint<out T>.withExtensions(block: T.(plugin: Plugin) -> Unit) {
internal inline fun <T : Extension> ExtensionPoint<out T>.withExtensions(block: T.(plugin: Plugin?) -> Unit) {
contract {
callsInPlace(block)
}
@ -128,11 +137,11 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
internal fun <T : Extension> ExtensionPoint<out T>.throwExtensionException(
extension: T,
plugin: Plugin,
plugin: Plugin?,
throwable: Throwable,
) {
throw ExtensionException(
"Exception while executing extension '${extension.kClassQualifiedNameOrTip}' provided by plugin '${plugin.name}', registered for '${this.extensionType.qualifiedName}'",
"Exception while executing extension '${extension.kClassQualifiedNameOrTip}' provided by plugin '${plugin?.name ?: "<builtin>"}', registered for '${this.extensionType.qualifiedName}'",
throwable
)
}
@ -142,7 +151,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
internal inline fun <T : Extension> ExtensionPoint<T>.useExtensions(block: (extension: T, plugin: Plugin) -> Unit): Unit =
internal inline fun <T : Extension> ExtensionPoint<T>.useExtensions(block: (extension: T, plugin: Plugin?) -> Unit): Unit =
withExtensions(block)
val instances: MutableMap<ExtensionPoint<*>, MutableSet<ExtensionRegistry<*>>> = ConcurrentHashMap()
@ -154,6 +163,15 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(DataExtensionRegistry(plugin, extensionInstance))
}
@JvmName("contribute1")
fun <T : Extension> contribute(
extensionPoint: ExtensionPoint<T>,
plugin: Plugin?,
extensionInstance: T,
) {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(DataExtensionRegistry(plugin, extensionInstance))
}
override fun <T : Extension> contribute(
extensionPoint: ExtensionPoint<T>,
plugin: Plugin,
@ -161,4 +179,13 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
) {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(LazyExtensionRegistry(plugin, lazyInstance))
}
@JvmName("contribute1")
fun <T : Extension> contribute(
extensionPoint: ExtensionPoint<T>,
plugin: Plugin?,
lazyInstance: () -> T,
) {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(LazyExtensionRegistry(plugin, lazyInstance))
}
}

View File

@ -48,7 +48,7 @@ internal abstract class JvmPluginInternal(
final override val parentPermission: Permission by lazy {
PermissionService.INSTANCE.register(
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION),
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.PLUGIN_ROOT_PERMISSION),
"The base permission"
)
}

View File

@ -148,7 +148,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
var count = 0
GlobalComponentStorage.run {
PluginLoaderProvider.useExtensions { ext, plugin ->
logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin.name}" }
logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin?.name ?: "<builtin>"}" }
_pluginLoaders.add(ext.instance)
count++
}

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.permission
import net.mamoe.mirai.console.command.BuiltInCommands
@ -65,10 +67,10 @@ public interface Permission {
* @see RootPermission 推荐 Kotlin 用户使用.
*/
@JvmStatic
public fun getRootPermission(): Permission = PermissionService.INSTANCE.rootPermission
public fun getRootPermission(): Permission = RootPermission
/**
* 递归获取 [Permission.parent], `permission.parent.parent`, permission.parent.parent` ... 直到 [Permission.parent] 为它自己.
* 递归获取 [Permission.parent], `permission.parent.parent`, permission.parent.parent.parent` ... 直到 [Permission.parent] 为它自己.
*/
@get:JvmStatic
public val Permission.parentsWithSelf: Sequence<Permission>
@ -82,5 +84,5 @@ public interface Permission {
* 根权限. 是所有权限的父权限. 权限 ID "*:*"
*/
@get:JvmSynthetic
public val RootPermission: Permission
public inline val RootPermission: Permission // It might be removed in the future, so make it inline to avoid ABI changes.
get() = PermissionService.INSTANCE.rootPermission

View File

@ -35,20 +35,20 @@ public data class PermissionId(
"' ' is not allowed in namespace"
}
require(name.none { it.isWhitespace() }) {
"' ' is not allowed in id"
"' ' is not allowed in name"
}
require(!namespace.contains(':')) {
"':' is not allowed in namespace"
}
require(!name.contains(':')) {
"':' is not allowed in id"
"':' is not allowed in name"
}
}
public object PermissionIdAsStringSerializer : KSerializer<PermissionId> by String.serializer().map(
serializer = { it.namespace + ":" + it.name },
deserializer = { it.split(':').let { (namespace, id) -> PermissionId(namespace, id) } }
deserializer = ::parseFromString
)
/**
@ -76,11 +76,11 @@ public data class PermissionId(
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) value: String) {
public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) name: String) {
when {
value.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.")
value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.name.")
value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.")
name.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.")
name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in PermissionId.name.")
name.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.")
}
}
@ -89,11 +89,11 @@ public data class PermissionId(
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) value: String) {
public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) namespace: String) {
when {
value.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.")
value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.namespace.")
value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.")
namespace.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.")
namespace.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces are not yet allowed in PermissionId.namespace.")
namespace.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.")
}
}
}

View File

@ -129,10 +129,10 @@ public interface PermissionService<P : Permission> {
/** [Plugin] 尝试分配的 [PermissionId] 来源 */
public enum class PluginPermissionIdRequestType {
/** For [Plugin.parentPermission] */
ROOT_PERMISSION,
PLUGIN_ROOT_PERMISSION,
/** For [Plugin.permissionId] */
PERMISSION_ID
NORMAL
}
public companion object {

View File

@ -39,7 +39,7 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor(
public final override val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader
public final override fun permissionId(name: String): PermissionId =
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.PERMISSION_ID)
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.NORMAL)
/**
* 重载 [PluginData]

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.console.util
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.data.qualifiedNameOrTip
import net.mamoe.mirai.contact.*
/**

View File

@ -0,0 +1,25 @@
/*
* 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.util
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageContent
@ConsoleExperimentalApi
public object MessageUtils {
@JvmStatic
public fun MessageChain.messageContentsSequence(): Sequence<MessageContent> = asSequence().filterIsInstance<MessageContent>()
@JvmStatic
public fun MessageChain.firstContent(): MessageContent = messageContentsSequence().first()
@JvmStatic
public fun MessageChain.firstContentOrNull(): MessageContent? = messageContentsSequence().firstOrNull()
}

View File

@ -0,0 +1,34 @@
/*
* 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.util
import kotlin.contracts.contract
/**
* Perform `this as? T`.
*/
@JvmSynthetic
public inline fun <reified T : Any> Any?.safeCast(): T? {
contract {
returnsNotNull() implies (this@safeCast is T)
}
return this as? T
}
/**
* Perform `this as T`.
*/
@JvmSynthetic
public inline fun <reified T : Any> Any?.cast(): T {
contract {
returns() implies (this@cast is T)
}
return this as T
}

View File

@ -89,7 +89,7 @@ internal object Testing {
internal var cont: Continuation<Any?>? = null
@Suppress("UNCHECKED_CAST")
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R {
suspend fun <R> withTesting(timeout: Long = 50000L, block: suspend () -> Unit): R {
@Suppress("RemoveExplicitTypeArguments") // bug
return if (timeout != -1L) {
withTimeout<R>(timeout) {

View File

@ -16,14 +16,14 @@ 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.execute
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.description.CommandArgumentParser
import net.mamoe.mirai.console.command.description.buildCommandArgumentContext
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
import net.mamoe.mirai.console.initTestEnvironment
import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.flattenCommandComponents
@ -38,7 +38,12 @@ object TestCompositeCommand : CompositeCommand(
"testComposite", "tsC"
) {
@SubCommand
fun mute(seconds: Int) {
fun mute(seconds: Int = 60) {
Testing.ok(seconds)
}
@SubCommand
fun mute(target: Long, seconds: Int) {
Testing.ok(seconds)
}
}
@ -54,6 +59,7 @@ internal val sender by lazy { ConsoleCommandSender }
internal val owner by lazy { ConsoleCommandOwner }
@OptIn(ExperimentalCommandDescriptors::class)
internal class TestCommand {
companion object {
@JvmStatic
@ -72,26 +78,31 @@ internal class TestCommand {
@Test
fun testRegister() {
try {
ConsoleCommandOwner.unregisterAllCommands() // builtins
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)
assertEquals(2,
CommandManagerImpl.requiredPrefixCommandMap.size,
CommandManagerImpl.requiredPrefixCommandMap.entries.joinToString { it.toString() })
} finally {
TestCompositeCommand.unregister()
unregisterCommand(TestCompositeCommand)
}
}
@Test
fun testSimpleExecute() = runBlocking {
TestSimpleCommand.withRegistration {
assertEquals("test", withTesting<MessageChain> {
assertSuccess(TestSimpleCommand.execute(sender, "test"))
}.contentToString())
}
}
@Test
fun `test flattenCommandArgs`() {
@ -105,15 +116,18 @@ internal class TestCommand {
@Test
fun testSimpleArgsSplitting() = runBlocking {
TestSimpleCommand.withRegistration {
assertEquals(arrayOf("test", "ttt", "tt").joinToString(), withTesting<MessageChain> {
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt")))
}.joinToString())
}
}
val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f")
@Test
fun `PlainText and Image args splitting`() = runBlocking {
TestSimpleCommand.withRegistration {
val result = withTesting<MessageChain> {
assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain {
+"test"
@ -124,6 +138,7 @@ internal class TestCommand {
assertEquals<Any>(arrayOf("test", image, "tt").joinToString(), result.toTypedArray().joinToString())
assertSame(image, result[1])
}
}
@Test
fun `test throw Exception`() {
@ -134,20 +149,30 @@ internal class TestCommand {
@Test
fun `executing command by string command`() = runBlocking {
TestCompositeCommand.register()
TestCompositeCommand.withRegistration {
val result = withTesting<Int> {
assertSuccess(sender.executeCommand("/testComposite mute 1"))
}
assertEquals(1, result)
}
}
@Test
fun `composite command descriptors`() {
val overloads = TestCompositeCommand.overloads
assertEquals("CommandSignatureVariant(<mute>, seconds: Int = ...)", overloads[0].toString())
assertEquals("CommandSignatureVariant(<mute>, target: Long, seconds: Int)", overloads[1].toString())
}
@Test
fun `composite command executing`() = runBlocking {
TestCompositeCommand.withRegistration {
assertEquals(1, withTesting {
assertSuccess(TestCompositeCommand.execute(sender, "mute 1"))
})
}
}
@Test
fun `composite sub command resolution conflict`() {
@ -164,19 +189,19 @@ internal class TestCommand {
@Suppress("UNUSED_PARAMETER")
@SubCommand
fun mute(seconds: Int, arg2: Int) {
fun mute(seconds: Int, arg2: Int = 1) {
Testing.ok(2)
}
}
assertFailsWith<IllegalStateException> {
composite.register()
}
/*
registerCommand(composite)
println(composite.overloads.joinToString())
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") })
}*/
assertEquals(1, withTesting { assertSuccess(composite.execute(sender, "mute 123")) }) // one arg, resolves to mute(Int)
assertEquals(2, withTesting { assertSuccess(composite.execute(sender, "mute 123 1")) }) // two arg, resolved to mute(Int, Int)
}
}
}
@ -184,19 +209,20 @@ internal class TestCommand {
fun `composite sub command parsing`() {
runBlocking {
class MyClass(
val value: Int
val value: Int,
)
val composite = object : CompositeCommand(
ConsoleCommandOwner,
"test22",
overrideContext = buildCommandArgumentContext {
add(object : CommandArgumentParser<MyClass> {
add(object : CommandValueArgumentParser<MyClass> {
override fun parse(raw: String, sender: CommandSender): MyClass {
return MyClass(raw.toInt())
}
override fun parse(raw: MessageContent, sender: CommandSender): MyClass {
if (raw is PlainText) return parse(raw.content, sender)
assertSame(image, raw)
return MyClass(2)
}
@ -210,12 +236,14 @@ internal class TestCommand {
}
composite.withRegistration {
assertEquals(333, withTesting<MyClass> { execute(sender, "mute 333") }.value)
assertEquals(333, withTesting<MyClass> { assertSuccess(execute(sender, "mute 333")) }.value)
assertEquals(2, withTesting<MyClass> {
assertSuccess(
execute(sender, buildMessageChain {
+"mute"
+image
})
)
}.value)
}
}
@ -238,8 +266,76 @@ internal class TestCommand {
}
}
}
@Test
fun `test optional argument command`() {
runBlocking {
val optionCommand = object : CompositeCommand(
ConsoleCommandOwner,
"testOptional"
) {
@SubCommand
fun optional(arg1: String, arg2: String = "Here is optional", arg3: String? = null) {
println(arg1)
println(arg2)
println(arg3)
// println(arg3)
Testing.ok(Unit)
}
}
optionCommand.withRegistration {
withTesting<Unit> {
assertSuccess(sender.executeCommand("/testOptional optional 1"))
}
}
}
}
internal fun assertSuccess(result: CommandExecuteResult) {
assertTrue(result.isSuccess(), result.toString())
@Test
fun `test vararg`() {
runBlocking {
val optionCommand = object : CompositeCommand(
ConsoleCommandOwner,
"test"
) {
@SubCommand
fun vararg(arg1: Int, vararg x: String) {
assertEquals(1, arg1)
Testing.ok(x)
}
}
optionCommand.withRegistration {
assertArrayEquals(
emptyArray<String>(),
withTesting {
assertSuccess(sender.executeCommand("/test vararg 1"))
}
)
assertArrayEquals(
arrayOf("s"),
withTesting<Array<String>> {
assertSuccess(sender.executeCommand("/test vararg 1 s"))
}
)
assertArrayEquals(
arrayOf("s", "s", "s"),
withTesting {
assertSuccess(sender.executeCommand("/test vararg 1 s s s"))
}
)
}
}
}
}
fun <T> assertArrayEquals(expected: Array<out T>, actual: Array<out T>, message: String? = null) {
asserter.assertEquals(message, expected.contentToString(), actual.contentToString())
}
@OptIn(ExperimentalCommandDescriptors::class)
internal fun assertSuccess(result: CommandExecuteResult) {
if (result.isFailure()) {
throw result.exception ?: AssertionError(result.toString())
}
}

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

@ -37,9 +37,9 @@
[`RawCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt
[`CommandManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt
[`CommandSender`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt
[`CommandArgumentParser`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParser.kt
[`CommandArgumentContext`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt
[`CommandArgumentContext.BuiltIns`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt#L66
[`CommandArgumentParser`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentParser.kt
[`CommandArgumentContext`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt
[`CommandArgumentContext.BuiltIns`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt#L66
[`MessageScope`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/MessageScope.kt
@ -113,7 +113,7 @@ interface CommandArgumentParser<out T : Any> {
支持原生数据类型,`Contact` 及其子类,`Bot`。
#### 构建 [`CommandArgumentContext`]
查看源码内注释:[CommandArgumentContext.kt: Line 146](../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentContext.kt#L146-L183)
查看源码内注释:[CommandArgumentContext.kt: Line 146](../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/descriptor/CommandArgumentContext.kt#L146-L183)
### 支持参数解析的 [`Command`] 实现
Mirai Console 内建 [`SimpleCommand`] 与 [`CompositeCommand`] 拥有 [`CommandArgumentContext`],在处理参数时会首先解析参数再传递给插件的实现。

View File

@ -15,11 +15,8 @@ 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
import net.mamoe.mirai.console.util.requestInput
@ -29,7 +26,7 @@ import org.jline.reader.UserInterruptException
val consoleLogger by lazy { DefaultLogger("console") }
@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class)
@OptIn(ConsoleInternalApi::class, ConsoleTerminalExperimentalApi::class, ExperimentalCommandDescriptors::class)
internal fun startupConsoleThread() {
if (terminal is NoConsole) return
@ -65,6 +62,9 @@ internal fun startupConsoleThread() {
when (result.status) {
CommandExecuteStatus.SUCCESSFUL -> {
}
CommandExecuteStatus.ILLEGAL_ARGUMENT -> {
result.exception?.message?.let { consoleLogger.warning(it) }
}
CommandExecuteStatus.EXECUTION_EXCEPTION -> {
result.exception?.let(consoleLogger::error)
}

View File

@ -1,2 +1,3 @@
# style guide
kotlin.code.style=official
org.gradle.vfs.watch=true

View File

@ -1,5 +1,5 @@
#Wed Mar 04 22:27:09 CST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists