Merge remote-tracking branch 'origin/master' into logger

This commit is contained in:
Karlatemp 2020-10-25 23:48:16 +08:00
commit b60ce7d856
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
83 changed files with 2772 additions and 1344 deletions

View File

@ -31,6 +31,7 @@ import java.util.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.annotation.AnnotationTarget.* import kotlin.annotation.AnnotationTarget.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
/** /**
@ -176,7 +177,7 @@ public interface MiraiConsoleImplementation : CoroutineScope {
/** /**
* 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例 * 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例
* *
* 必须在 [start] 之后才能使用. * 必须在 [start] 之后才能使用, 否则抛出 [UninitializedPropertyAccessException]
*/ */
@JvmStatic @JvmStatic
@ConsoleFrontEndImplementation @ConsoleFrontEndImplementation
@ -189,7 +190,22 @@ public interface MiraiConsoleImplementation : CoroutineScope {
public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock { public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock {
if (::instance.isInitialized) error("Mirai Console is already initialized.") if (::instance.isInitialized) error("Mirai Console is already initialized.")
this@Companion.instance = this this@Companion.instance = this
MiraiConsoleImplementationBridge.doStart() kotlin.runCatching {
MiraiConsoleImplementationBridge.doStart()
}.onFailure { e ->
kotlin.runCatching {
MiraiConsole.mainLogger.error("Failed to init MiraiConsole.", e)
}.onFailure {
e.printStackTrace()
}
kotlin.runCatching {
MiraiConsole.cancel()
}.onFailure {
it.printStackTrace()
}
exitProcess(1)
}
} }
} }
} }

View File

@ -15,16 +15,20 @@ import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException
import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser.Companion.map
import net.mamoe.mirai.console.command.descriptor.PermissionIdValueArgumentParser
import net.mamoe.mirai.console.command.descriptor.PermitteeIdValueArgumentParser
import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext
import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands
import net.mamoe.mirai.console.internal.util.runIgnoreException import net.mamoe.mirai.console.internal.util.runIgnoreException
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.permission.PermissionService.Companion.denyPermission import net.mamoe.mirai.console.permission.PermissionService.Companion.cancel
import net.mamoe.mirai.console.permission.PermissionService.Companion.findCorrespondingPermissionOrFail import net.mamoe.mirai.console.permission.PermissionService.Companion.findCorrespondingPermissionOrFail
import net.mamoe.mirai.console.permission.PermissionService.Companion.getPermittedPermissions import net.mamoe.mirai.console.permission.PermissionService.Companion.getPermittedPermissions
import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission import net.mamoe.mirai.console.permission.PermissionService.Companion.permit
import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.ConsoleInternalApi
@ -150,11 +154,11 @@ public object BuiltInCommands {
ConsoleCommandOwner, "permission", "权限", "perm", ConsoleCommandOwner, "permission", "权限", "perm",
description = "管理权限", description = "管理权限",
overrideContext = buildCommandArgumentContext { overrideContext = buildCommandArgumentContext {
PermitteeId::class with PermitteeIdArgumentParser PermitteeId::class with PermitteeIdValueArgumentParser
Permission::class with PermissionIdArgumentParser.map { id -> Permission::class with PermissionIdValueArgumentParser.map { id ->
kotlin.runCatching { kotlin.runCatching {
id.findCorrespondingPermissionOrFail() id.findCorrespondingPermissionOrFail()
}.getOrElse { illegalArgument("指令不存在: $id", it) } }.getOrElse { throw CommandArgumentParserException("指令不存在: $id", it) }
} }
}, },
), BuiltInCommandInternal { ), BuiltInCommandInternal {
@ -166,7 +170,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId, @Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission, @Name("权限 ID") permission: Permission,
) { ) {
target.grantPermission(permission) target.permit(permission)
sendMessage("OK") sendMessage("OK")
} }
@ -176,7 +180,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId, @Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission, @Name("权限 ID") permission: Permission,
) { ) {
target.denyPermission(permission, false) target.cancel(permission, false)
sendMessage("OK") sendMessage("OK")
} }
@ -186,7 +190,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId, @Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission, @Name("权限 ID") permission: Permission,
) { ) {
target.denyPermission(permission, true) target.cancel(permission, true)
sendMessage("OK") sendMessage("OK")
} }

View File

@ -11,26 +11,25 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.console.command.descriptor.CommandArgumentContextAware
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.descriptor.CommandSignature
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 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
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionId 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 RawCommand 无参数解析, 接收原生参数的指令
* @see CompositeCommand 复合指令 * @see CompositeCommand 复合指令
* @see SimpleCommand 简单的, 支持参数自动解析的指令 * @see SimpleCommand 简单的, 支持参数自动解析的指令
* *
* @see JCommand Java 用户添加协程帮助的 [Command] * @see CommandArgumentContextAware
*/ */
public interface Command { public interface Command {
/** /**
@ -48,18 +47,25 @@ public interface Command {
@ResolveContext(COMMAND_NAME) @ResolveContext(COMMAND_NAME)
public val secondaryNames: Array<out String> public val secondaryNames: Array<out String>
/**
* 指令可能的参数列表.
*/
@ConsoleExperimentalApi("Property name is experimental")
@ExperimentalCommandDescriptors
public val overloads: List<CommandSignature>
/** /**
* 用法说明, 用于发送给用户. [usage] 一般包含 [description]. * 用法说明, 用于发送给用户. [usage] 一般包含 [description].
*/ */
public val usage: String public val usage: String
/** /**
* 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] * 描述, 用于显示在 [BuiltInCommands.HelpCommand]
*/ */
public val description: String public val description: String
/** /**
* 此指令分配的权限. * 此指令分配的权限.
* *
* ### 实现约束 * ### 实现约束
* - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace] * - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace]
@ -72,6 +78,8 @@ public interface Command {
* *
* 会影响聊天语境中的解析. * 会影响聊天语境中的解析.
*/ */
@ExperimentalCommandDescriptors
@ConsoleExperimentalApi
public val prefixOptional: Boolean public val prefixOptional: Boolean
/** /**
@ -80,16 +88,6 @@ public interface Command {
*/ */
public val owner: CommandOwner public val owner: CommandOwner
/**
* 在指令被执行时调用.
*
* @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数.
*
* @see CommandManager.executeCommand 查看更多信息
*/
@JvmBlockingBridge
public suspend fun CommandSender.onCommand(args: MessageChain)
public companion object { public companion object {
/** /**
@ -109,19 +107,10 @@ public interface Command {
public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) { public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) {
when { when {
name.isBlank() -> throw IllegalArgumentException("Command name should not be blank.") 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.")
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 package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus 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.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import kotlin.contracts.contract import kotlin.contracts.contract
@ -21,6 +23,8 @@ import kotlin.contracts.contract
* *
* @see CommandExecuteStatus * @see CommandExecuteStatus
*/ */
@ConsoleExperimentalApi("Not yet implemented")
@ExperimentalCommandDescriptors
public sealed class CommandExecuteResult { public sealed class CommandExecuteResult {
/** 指令最终执行状态 */ /** 指令最终执行状态 */
public abstract val status: CommandExecuteStatus public abstract val status: CommandExecuteStatus
@ -55,6 +59,21 @@ public sealed class CommandExecuteResult {
public override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL 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( 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() { ) : CommandExecuteResult() {
/** 指令执行时发生的错误, 总是 `null` */ /** 指令执行时发生的错误, 总是 `null` */
public override val exception: Nothing? get() = null public override val exception: Nothing? get() = null
@ -119,7 +138,9 @@ public sealed class CommandExecuteResult {
COMMAND_NOT_FOUND, COMMAND_NOT_FOUND,
/** 权限不足 */ /** 权限不足 */
PERMISSION_DENIED PERMISSION_DENIED,
/** 非法参数 */
ILLEGAL_ARGUMENT,
} }
} }
@ -138,6 +159,18 @@ public fun CommandExecuteResult.isSuccess(): Boolean {
return this is CommandExecuteResult.Success 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` * [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true`
*/ */
@ -151,7 +184,7 @@ public fun CommandExecuteResult.isExecutionException(): Boolean {
} }
/** /**
* [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true` * [this] [CommandExecuteResult.PermissionDenied] 时返回 `true`
*/ */
@JvmSynthetic @JvmSynthetic
public fun CommandExecuteResult.isPermissionDenied(): Boolean { public fun CommandExecuteResult.isPermissionDenied(): Boolean {
@ -163,19 +196,19 @@ public fun CommandExecuteResult.isPermissionDenied(): Boolean {
} }
/** /**
* [this] [CommandExecuteResult.ExecutionFailed] 时返回 `true` * [this] [CommandExecuteResult.UnresolvedCall] 时返回 `true`
*/ */
@JvmSynthetic @JvmSynthetic
public fun CommandExecuteResult.isCommandNotFound(): Boolean { public fun CommandExecuteResult.isCommandNotFound(): Boolean {
contract { contract {
returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound) returns(true) implies (this@isCommandNotFound is CommandExecuteResult.UnresolvedCall)
returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound) 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 @JvmSynthetic
public fun CommandExecuteResult.isFailure(): Boolean { public fun CommandExecuteResult.isFailure(): Boolean {

View File

@ -8,7 +8,7 @@
*/ */
@file:Suppress( @file:Suppress(
"NOTHING_TO_INLINE", "unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE", "NOTHING_TO_INLINE", "unused",
"MemberVisibilityCanBePrivate", "INAPPLICABLE_JVM_NAME" "MemberVisibilityCanBePrivate", "INAPPLICABLE_JVM_NAME"
) )
@file:JvmName("CommandManagerKt") @file:JvmName("CommandManagerKt")
@ -16,21 +16,21 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.kjbb.JvmBlockingBridge 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
import net.mamoe.mirai.console.internal.command.CommandManagerImpl.executeCommand 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.* import net.mamoe.mirai.message.data.*
/** /**
* 指令管理器 * 指令管理器
*/ */
public interface CommandManager { public interface CommandManager {
/**
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
*
* @return 这一时刻的浅拷贝.
*/
public val CommandOwner.registeredCommands: List<Command>
/** /**
* 获取所有已经注册了指令列表. * 获取所有已经注册了指令列表.
* *
@ -44,9 +44,17 @@ public interface CommandManager {
public val commandPrefix: String 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] 也可以被覆盖. * 注意: [内建指令][BuiltInCommands] 也可以被覆盖.
*/ */
@JvmName("registerCommand") public fun registerCommand(command: Command, override: Boolean = false): Boolean
public fun Command.register(override: Boolean = false): Boolean
/** /**
* 查找并返回重名的指令. 返回重名指令. * 查找并返回重名的指令. 返回重名指令.
*/ */
@JvmName("findCommandDuplicate") public fun findDuplicateCommand(command: Command): Command?
public fun Command.findDuplicate(): Command?
/** /**
* 取消注册这个指令. * 取消注册这个指令.
* *
* 若指令未注册, 返回 `false`. * 若指令未注册, 返回 `false`.
*/ */
@JvmName("unregisterCommand") public fun unregisterCommand(command: Command): Boolean
public fun Command.unregister(): Boolean
/** /**
* [this] 已经 [注册][register] 时返回 `true` * [command] 已经 [注册][registerCommand] 时返回 `true`
*/ */
@JvmName("isCommandRegistered") public fun isCommandRegistered(command: Command): Boolean
public fun Command.isRegistered(): Boolean
/** /**
* 解析并执行一个指令. * 解析并执行一个指令.
* *
* 如要避免参数解析, 请使用 [Command.onCommand]
*
* ### 指令解析流程 * ### 指令解析流程
* 1. [CommandCallParser] [MessageChain] 解析为 [CommandCall]
* 2. [CommandCallResolver] [CommandCall] 解析为 []
* 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理) * 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理)
* 2. 参数语法分析. * 2. 参数语法分析.
* 在当前的实现下, [message] 被以空格和 [SingleMessage] 分割. * 在当前的实现下, [message] 被以空格和 [SingleMessage] 分割.
@ -101,118 +105,154 @@ public interface CommandManager {
* 注意: 字符串与消息元素之间不需要空格, 会被强制分割. "bar[mirai:image:]" 会被分割为 "bar" [Image] 类型的消息元素. * 注意: 字符串与消息元素之间不需要空格, 会被强制分割. "bar[mirai:image:]" 会被分割为 "bar" [Image] 类型的消息元素.
* 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand] * 3. 参数解析. 各类型指令实现不同. 详见 [RawCommand], [CompositeCommand], [SimpleCommand]
* *
* ### 未来的扩展 * ### 扩展
* 在将来, 参数语法分析过程可能会被扩展, 允许插件自定义处理方式, 因此可能不会简单地使用空格分隔. * 参数语法分析过程可能会被扩展, 插件可以自定义处理方式 ([CommandCallParser]), 因此可能不会简单地使用空格分隔.
* *
* @param message 一条完整的指令. "/managers add 123456.123456" * @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限 * @param checkPermission `true` 时检查权限
* *
* @see CommandCallParser
* @see CommandCallResolver
*
* @see CommandSender.executeCommand
* @see Command.execute
*
* @return 执行结果 * @return 执行结果
*/ */
@ExperimentalCommandDescriptors
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun CommandSender.executeCommand( public suspend fun executeCommand(
caller: CommandSender,
message: Message, message: Message,
checkPermission: Boolean = true, checkPermission: Boolean = true,
): CommandExecuteResult ): CommandExecuteResult {
return executeCommandImpl(message, caller, checkPermission)
/** }
* 解析并执行一个指令
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @return 执行结果
* @see executeCommand
*/
@JvmBlockingBridge
public suspend fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true,
): CommandExecuteResult = executeCommand(PlainText(message).asMessageChain(), checkPermission)
/** /**
* 执行一个确切的指令 * 执行一个确切的指令
*
* @param command 目标指令
* @param arguments 参数列表
*
* @see executeCommand 获取更多信息 * @see executeCommand 获取更多信息
* @see Command.execute
*/ */
@JvmBlockingBridge @ConsoleExperimentalApi
@JvmName("executeCommand") @JvmName("executeCommand")
public suspend fun Command.execute( @ExperimentalCommandDescriptors
@JvmSynthetic
public suspend fun executeCommand(
sender: CommandSender, sender: CommandSender,
command: Command,
arguments: Message = EmptyMessageChain, arguments: Message = EmptyMessageChain,
checkPermission: Boolean = true, checkPermission: Boolean = true,
): CommandExecuteResult ): 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].
* @see executeCommand 获取更多信息 *
* #### 实现细节
* - [commandName] 带有 [commandPrefix] 时可以匹配到所有指令
* - [commandName] 不带有 [commandPrefix] 时只能匹配到 [Command.prefixOptional] 的指令
*
* @param commandName 可能带有或不带有 [commandPrefix].
*/ */
@JvmBlockingBridge public fun matchCommand(commandName: String): Command?
@JvmName("executeCommand")
public suspend fun Command.execute(
sender: CommandSender,
arguments: String = "",
checkPermission: Boolean = true,
): CommandExecuteResult = execute(sender, PlainText(arguments).asMessageChain(), checkPermission)
public companion object INSTANCE : CommandManager by CommandManagerImpl { 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 CommandManager.getRegisteredCommands
* @see execute 获取更多信息
*/ */
public suspend fun CommandSender.execute( @get:JvmName("registeredCommands0")
command: Command, @get:JvmSynthetic
arguments: Message, public inline val CommandOwner.registeredCommands: List<Command>
checkPermission: Boolean = true, get() = getRegisteredCommands(this)
): CommandExecuteResult {
return command.execute(this, arguments, checkPermission)
}
/** /**
* 执行一个确切的指令 * @see CommandManager.registerCommand
* @see execute 获取更多信息
*/ */
public suspend fun CommandSender.execute( @JvmSynthetic
command: Command, public inline fun Command.register(override: Boolean = false): Boolean = registerCommand(this, override)
arguments: String,
checkPermission: Boolean = true, /**
): CommandExecuteResult { * @see CommandManager.unregisterCommand
return command.execute(this, arguments, checkPermission) */
} @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)
} }
} }
/**
* 解析并执行一个指令
*
* @param message 一条完整的指令. "/managers add 123456.123456"
* @param checkPermission `true` 时检查权限
*
* @return 执行结果
* @see executeCommand
*/
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun CommandSender.executeCommand(
message: String,
checkPermission: Boolean = true,
): CommandExecuteResult = CommandManager.executeCommand(this, PlainText(message).asMessageChain(), checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun Command.execute(
sender: CommandSender,
arguments: Message = EmptyMessageChain,
checkPermission: Boolean = true,
): CommandExecuteResult = CommandManager.executeCommand(sender, this, arguments, checkPermission)
/**
* 执行一个确切的指令
* @see executeCommand 获取更多信息
*/
@JvmName("execute0")
@ExperimentalCommandDescriptors
@JvmSynthetic
public suspend inline fun Command.execute(
sender: CommandSender,
arguments: String = "",
checkPermission: Boolean = true,
): CommandExecuteResult = execute(sender, PlainText(arguments), checkPermission)

View File

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

View File

@ -17,14 +17,13 @@
package net.mamoe.mirai.console.command 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
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME 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.internal.command.CompositeCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain
import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.annotation.AnnotationTarget.FUNCTION
@ -90,16 +89,28 @@ public abstract class CompositeCommand(
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false, prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), ) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware { CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, CompositeCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureFromKFunction> by lazy {
reflector.findSubCommands().also {
reflector.validate(it)
}
}
/** /**
* 自动根据带有 [SubCommand] 注解的函数签名生成 [usage]. 也可以被覆盖. * 自动根据带有 [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 public final override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext
@ -123,20 +134,6 @@ public abstract class CompositeCommand(
@Retention(RUNTIME) @Retention(RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
protected annotation class Name(val value: String) 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

@ -5,23 +5,25 @@
* 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. * 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 * https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/ */
@file:Suppress("unused") @file:Suppress("unused")
package net.mamoe.mirai.console.command.description package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException
/** /**
* 解析参数时遇到的 _正常_ 错误. 如参数不符合规范. * 处理参数时遇到的 _正常_ 错误. 如参数不符合规范, 参数值越界.
* *
* [message] 将会发送给指令调用方. * [message] 将会发送给指令调用方.
* *
* @see CommandArgumentParser * @see CommandArgumentParserException
* @see CommandArgumentParser.illegalArgument
*/ */
public class CommandArgumentParserException : RuntimeException { public open class IllegalCommandArgumentException : IllegalArgumentException {
public constructor() : super() public constructor() : super()
public constructor(message: String?) : super(message) public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause) public constructor(message: String?, cause: Throwable?) : super(message, cause)
public constructor(cause: Throwable?) : super(cause) public constructor(cause: Throwable?) : super(cause)
} }

View File

@ -11,14 +11,16 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.java.JRawCommand import net.mamoe.mirai.console.command.java.JRawCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME 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.command.createOrFindCommandPermission
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.permission.Permission 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.MessageChain
import net.mamoe.mirai.message.data.buildMessageChain
/** /**
* 无参数解析, 接收原生参数的指令. * 无参数解析, 接收原生参数的指令.
@ -48,10 +50,23 @@ public abstract class RawCommand(
/** 指令父权限 */ /** 指令父权限 */
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@OptIn(ExperimentalCommandDescriptors::class)
public override val prefixOptional: Boolean = false, public override val prefixOptional: Boolean = false,
) : Command { ) : Command {
public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) } public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) }
@ExperimentalCommandDescriptors
override val overloads: List<CommandSignature> = listOf(
CommandSignatureImpl(
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 查看更多信息 * @see CommandManager.execute 查看更多信息
*/ */
public abstract override suspend fun CommandSender.onCommand(args: MessageChain) public abstract suspend fun CommandSender.onCommand(args: MessageChain)
} }

View File

@ -17,21 +17,23 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.java.JSimpleCommand import net.mamoe.mirai.console.command.java.JSimpleCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME 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.internal.command.SimpleCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission 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] * 要查看指令解析流程, 参考 [CommandManager.executeCommand]
* 要查看参数解析方式, 参考 [CommandArgumentParser] * 要查看参数解析方式, 参考 [CommandValueArgumentParser]
* *
* Kotlin 实现: * Kotlin 实现:
* ``` * ```
@ -58,39 +60,42 @@ public abstract class SimpleCommand(
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false, prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional), ) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware { CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<CommandSignatureFromKFunction> by lazy {
reflector.findSubCommands().also {
reflector.validate(it)
if (it.isEmpty())
throw IllegalCommandDeclarationException(this, "SimpleCommand must have at least one subcommand, whereas zero present.")
}
}
/** /**
* 自动根据带有 [Handler] 注解的函数签名生成 [usage]. 也可以被覆盖. * 自动根据带有 [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 protected annotation class Handler
/** 参数名, 将参与构成 [usage] */
@ConsoleExperimentalApi("Classname might change")
@Target(VALUE_PARAMETER)
protected annotation class Name(val value: String)
/** /**
* 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext` * 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext`
*/ */
public override val context: CommandArgumentContext = 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

@ -1,145 +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", "unused")
package net.mamoe.mirai.console.command.description
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.content
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* 指令参数解析器. 用于解析字符串或 [SingleMessage] 到特定参数类型.
*
* ### 参数解析
*
* [SimpleCommand] 中的示例:
* ```
* suspend fun CommandSender.mute(target: Member, duration: Int)
* ```
* [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] [Member] [CommandArgumentParser], 并调用其 [CommandArgumentParser.parse]
*
* ### 内建指令解析器
* - 基础类型: [ByteArgumentParser], [ShortArgumentParser], [IntArgumentParser], [LongArgumentParser]
* [FloatArgumentParser], [DoubleArgumentParser],
* [BooleanArgumentParser], [StringArgumentParser]
*
* - [Bot]: [ExistingBotArgumentParser]
* - [Friend]: [ExistingFriendArgumentParser]
* - [Group]: [ExistingGroupArgumentParser]
* - [Member]: [ExistingMemberArgumentParser]
* - [User]: [ExistingUserArgumentParser]
* - [Contact]: [ExistingContactArgumentParser]
*
*
* @see SimpleCommand 简单指令
* @see CompositeCommand 复合指令
*
* @see buildCommandArgumentContext 指令参数环境, [CommandArgumentParser] 的集合
*/
public interface CommandArgumentParser<out T : Any> {
/**
* 解析一个字符串为 [T] 类型参数
*
* **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException].
* 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户.
*
* @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出.
*
* @see CommandArgumentParserException
*/
@Throws(CommandArgumentParserException::class)
public fun parse(raw: String, sender: CommandSender): T
/**
* 解析一个消息内容元素为 [T] 类型参数
*
* **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException].
* 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户.
*
* @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出.
*
* @see CommandArgumentParserException
*/
@Throws(CommandArgumentParserException::class)
public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender)
}
/**
* 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型.
*/
public fun <T : Any, R : Any> CommandArgumentParser<T>.map(
mapper: CommandArgumentParser<R>.(T) -> R
): CommandArgumentParser<R> = MappingCommandArgumentParser(this, mapper)
private class MappingCommandArgumentParser<T : Any, R : Any>(
private val original: CommandArgumentParser<T>,
private val mapper: CommandArgumentParser<R>.(T) -> R
) : CommandArgumentParser<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))
}
/**
* 解析一个字符串或 [SingleMessage] [T] 类型参数
*
* @throws IllegalArgumentException [raw] 既不是 [SingleMessage], 也不是 [String] 时抛出.
*/
@JvmSynthetic
@Throws(IllegalArgumentException::class)
public fun <T : Any> CommandArgumentParser<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)
else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}")
}
}
/**
* 抛出一个 [CommandArgumentParserException] 的捷径
*
* @throws CommandArgumentParserException
*/
@Suppress("unused")
@JvmSynthetic
@Throws(CommandArgumentParserException::class)
public inline fun CommandArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing {
throw CommandArgumentParserException(message, cause)
}
/**
* 检查参数 [condition]. 当它为 `false` 时调用 [message] 并以其返回值作为消息, 抛出异常 [CommandArgumentParserException]
*
* @throws CommandArgumentParserException
*/
@Throws(CommandArgumentParserException::class)
@JvmSynthetic
public inline fun CommandArgumentParser<*>.checkArgument(
condition: Boolean,
crossinline message: () -> String = { "Check failed." }
) {
contract {
returns() implies condition
callsInPlace(message, InvocationKind.AT_MOST_ONCE)
}
if (!condition) illegalArgument(message())
}

View File

@ -9,26 +9,29 @@
@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") @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.Bot
import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.SimpleCommand 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.PermissionId
import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.PlainText
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.internal.LowPriorityInOverloadResolution import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
/** /**
* 指令参数环境, [CommandArgumentParser] 的集合, 用于 [CompositeCommand] [SimpleCommand]. * 指令参数环境, [CommandValueArgumentParser] 的集合, 用于 [CompositeCommand] [SimpleCommand].
* *
* 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器 * 在指令解析时, 总是从 [CommandArgumentContextAware.context] 搜索相关解析器
* *
@ -37,20 +40,28 @@ import kotlin.reflect.full.isSubclassOf
* @see SimpleCommandArgumentContext 简单实现 * @see SimpleCommandArgumentContext 简单实现
* @see EmptyCommandArgumentContext 空实现, 类似 [emptyList] * @see EmptyCommandArgumentContext 空实现, 类似 [emptyList]
* *
* @see CommandArgumentContext.Builtins 内建 [CommandArgumentParser] * @see CommandArgumentContext.Builtins 内建 [CommandValueArgumentParser]
* *
* @see buildCommandArgumentContext DSL 构造 * @see buildCommandArgumentContext DSL 构造
*/ */
public interface CommandArgumentContext { public interface CommandArgumentContext {
/** /**
* [KClass] [CommandArgumentParser] 的匹配 * [KClass] [CommandValueArgumentParser] 的匹配
*/ */
public data class ParserPair<T : Any>( public data class ParserPair<T : Any>(
val klass: KClass<T>, val klass: KClass<T>,
val parser: CommandArgumentParser<T>, val parser: CommandValueArgumentParser<T>,
) ) {
public companion object {
@JvmStatic
public fun <T : Any> ParserPair<T>.toPair(): Pair<KClass<T>, CommandValueArgumentParser<T>> = klass to parser
}
}
public operator fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? /**
* 获取一个 [kClass] 类型的解析器.
*/
public operator fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>?
public fun toList(): List<ParserPair<*>> public fun toList(): List<ParserPair<*>>
@ -58,37 +69,39 @@ public interface CommandArgumentContext {
/** /**
* For Java callers. * For Java callers.
* *
* @see [EmptyCommandArgumentContext] * @see EmptyCommandArgumentContext
*/ */
@JvmStatic @JvmStatic
public val EMPTY: CommandArgumentContext = EmptyCommandArgumentContext public val EMPTY: CommandArgumentContext = EmptyCommandArgumentContext
} }
/** /**
* 内建的默认 [CommandArgumentParser] * 内建的默认 [CommandValueArgumentParser]
*/ */
public object Builtins : CommandArgumentContext by (buildCommandArgumentContext { public object Builtins : CommandArgumentContext by (buildCommandArgumentContext {
Int::class with IntArgumentParser Int::class with IntValueArgumentParser
Byte::class with ByteArgumentParser Byte::class with ByteValueArgumentParser
Short::class with ShortArgumentParser Short::class with ShortValueArgumentParser
Boolean::class with BooleanArgumentParser Boolean::class with BooleanValueArgumentParser
String::class with StringArgumentParser String::class with StringValueArgumentParser
Long::class with LongArgumentParser Long::class with LongValueArgumentParser
Double::class with DoubleArgumentParser Double::class with DoubleValueArgumentParser
Float::class with FloatArgumentParser Float::class with FloatValueArgumentParser
Image::class with ImageArgumentParser Image::class with ImageValueArgumentParser
PlainText::class with PlainTextArgumentParser PlainText::class with PlainTextValueArgumentParser
Contact::class with ExistingContactArgumentParser Contact::class with ExistingContactValueArgumentParser
User::class with ExistingUserArgumentParser User::class with ExistingUserValueArgumentParser
Member::class with ExistingMemberArgumentParser Member::class with ExistingMemberValueArgumentParser
Group::class with ExistingGroupArgumentParser Group::class with ExistingGroupValueArgumentParser
Friend::class with ExistingFriendArgumentParser Friend::class with ExistingFriendValueArgumentParser
Bot::class with ExistingBotArgumentParser Bot::class with ExistingBotValueArgumentParser
PermissionId::class with PermissionIdArgumentParser PermissionId::class with PermissionIdValueArgumentParser
PermitteeId::class with PermitteeIdArgumentParser PermitteeId::class with PermitteeIdValueArgumentParser
MessageContent::class with RawContentValueArgumentParser
}) })
} }
@ -100,11 +113,14 @@ public interface CommandArgumentContext {
*/ */
public interface CommandArgumentContextAware { public interface CommandArgumentContextAware {
/** /**
* [CommandArgumentParser] 的集合 * [CommandValueArgumentParser] 的集合
*/ */
public val context: CommandArgumentContext public val context: CommandArgumentContext
} }
/**
* @see CommandArgumentContext.EMPTY
*/
public object EmptyCommandArgumentContext : CommandArgumentContext by SimpleCommandArgumentContext(listOf()) public object EmptyCommandArgumentContext : CommandArgumentContext by SimpleCommandArgumentContext(listOf())
/** /**
@ -114,8 +130,8 @@ public operator fun CommandArgumentContext.plus(replacer: CommandArgumentContext
if (replacer == EmptyCommandArgumentContext) return this if (replacer == EmptyCommandArgumentContext) return this
if (this == EmptyCommandArgumentContext) return replacer if (this == EmptyCommandArgumentContext) return replacer
return object : CommandArgumentContext { return object : CommandArgumentContext {
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? = override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
replacer[klass] ?: this@plus[klass] replacer[kClass] ?: this@plus[kClass]
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList() override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
} }
@ -129,9 +145,9 @@ public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>):
if (this == EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer) if (this == EmptyCommandArgumentContext) return SimpleCommandArgumentContext(replacer)
return object : CommandArgumentContext { return object : CommandArgumentContext {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? = override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgumentParser<T>? replacer.firstOrNull { kClass.isSubclassOf(it.klass) }?.parser as CommandValueArgumentParser<T>?
?: this@plus[klass] ?: this@plus[kClass]
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList() override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
} }
@ -146,9 +162,9 @@ public operator fun CommandArgumentContext.plus(replacer: List<ParserPair<*>>):
public class SimpleCommandArgumentContext( public class SimpleCommandArgumentContext(
public val list: List<ParserPair<*>>, public val list: List<ParserPair<*>>,
) : CommandArgumentContext { ) : CommandArgumentContext {
override fun <T : Any> get(klass: KClass<out T>): CommandArgumentParser<T>? = override fun <T : Any> get(kClass: KClass<T>): CommandValueArgumentParser<T>? =
(this.list.firstOrNull { klass == it.klass }?.parser (this.list.firstOrNull { kClass == it.klass }?.parser
?: this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser) as CommandArgumentParser<T>? ?: this.list.firstOrNull { kClass.isSubclassOf(it.klass) }?.parser) as CommandValueArgumentParser<T>?
override fun toList(): List<ParserPair<*>> = list override fun toList(): List<ParserPair<*>> = list
} }
@ -160,7 +176,7 @@ public class SimpleCommandArgumentContext(
* ``` * ```
* val context = buildCommandArgumentContext { * val context = buildCommandArgumentContext {
* Int::class with IntArgParser * Int::class with IntArgParser
* Member::class with ExistMemberArgParser * Member::class with ExistingMemberArgParser
* Group::class with { s: String, sender: CommandSender -> * Group::class with { s: String, sender: CommandSender ->
* Bot.getInstance(s.toLong()).getGroup(s.toLong()) * Bot.getInstance(s.toLong()).getGroup(s.toLong())
* } * }
@ -189,6 +205,9 @@ public class SimpleCommandArgumentContext(
*/ */
@JvmSynthetic @JvmSynthetic
public fun buildCommandArgumentContext(block: CommandArgumentContextBuilder.() -> Unit): CommandArgumentContext { public fun buildCommandArgumentContext(block: CommandArgumentContextBuilder.() -> Unit): CommandArgumentContext {
contract {
callsInPlace(block, EXACTLY_ONCE)
}
return CommandArgumentContextBuilder().apply(block).build() return CommandArgumentContextBuilder().apply(block).build()
} }
@ -200,14 +219,14 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
* 添加一个指令解析器. * 添加一个指令解析器.
*/ */
@JvmName("add") @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 this.kotlin with parser
/** /**
* 添加一个指令解析器 * 添加一个指令解析器
*/ */
@JvmName("add") @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)) add(ParserPair(this, parser))
return this@CommandArgumentContextBuilder return this@CommandArgumentContextBuilder
} }
@ -218,9 +237,9 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@JvmSynthetic @JvmSynthetic
@LowPriorityInOverloadResolution @LowPriorityInOverloadResolution
public inline infix fun <T : Any> KClass<T>.with( 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 { ): CommandArgumentContextBuilder {
add(ParserPair(this, object : CommandArgumentParser<T> { add(ParserPair(this, object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
})) }))
return this@CommandArgumentContextBuilder return this@CommandArgumentContextBuilder
@ -231,16 +250,16 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
*/ */
@JvmSynthetic @JvmSynthetic
public inline infix fun <T : Any> KClass<T>.with( public inline infix fun <T : Any> KClass<T>.with(
crossinline parser: CommandArgumentParser<T>.(s: String) -> T, crossinline parser: CommandValueArgumentParser<T>.(s: String) -> T,
): CommandArgumentContextBuilder { ): CommandArgumentContextBuilder {
add(ParserPair(this, object : CommandArgumentParser<T> { add(ParserPair(this, object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw) override fun parse(raw: String, sender: CommandSender): T = parser(raw)
})) }))
return this@CommandArgumentContextBuilder return this@CommandArgumentContextBuilder
} }
@JvmSynthetic @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)) add(ParserPair(T::class, parser))
return this@CommandArgumentContextBuilder return this@CommandArgumentContextBuilder
} }
@ -251,8 +270,8 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@ConsoleExperimentalApi @ConsoleExperimentalApi
@JvmSynthetic @JvmSynthetic
public inline infix fun <reified T : Any> add( public inline infix fun <reified T : Any> add(
crossinline parser: CommandArgumentParser<*>.(s: String) -> T, crossinline parser: CommandValueArgumentParser<*>.(s: String) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser<T> { ): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw) override fun parse(raw: String, sender: CommandSender): T = parser(raw)
} }
@ -263,8 +282,8 @@ public class CommandArgumentContextBuilder : MutableList<ParserPair<*>> by mutab
@JvmSynthetic @JvmSynthetic
@LowPriorityInOverloadResolution @LowPriorityInOverloadResolution
public inline infix fun <reified T : Any> add( public inline infix fun <reified T : Any> add(
crossinline parser: CommandArgumentParser<*>.(s: String, sender: CommandSender) -> T, crossinline parser: CommandValueArgumentParser<*>.(s: String, sender: CommandSender) -> T,
): CommandArgumentContextBuilder = T::class with object : CommandArgumentParser<T> { ): CommandArgumentContextBuilder = T::class with object : CommandValueArgumentParser<T> {
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender) override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
} }

View File

@ -7,7 +7,9 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
package net.mamoe.mirai.console.command.description @file:Suppress("EXPOSED_SUPER_CLASS")
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
@ -25,47 +27,47 @@ import net.mamoe.mirai.message.data.*
/** /**
* 使用 [String.toInt] 解析 * 使用 [String.toInt] 解析
*/ */
public object IntArgumentParser : InternalCommandArgumentParserExtensions<Int> { public object IntValueArgumentParser : InternalCommandValueArgumentParserExtensions<Int>() {
public override fun parse(raw: String, sender: CommandSender): Int = public override fun parse(raw: String, sender: CommandSender): Int =
raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数") 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 = public override fun parse(raw: String, sender: CommandSender): Long =
raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数") 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 = public override fun parse(raw: String, sender: CommandSender): Short =
raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数") 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 = public override fun parse(raw: String, sender: CommandSender): Byte =
raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节") 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 = public override fun parse(raw: String, sender: CommandSender): Double =
raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数") 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 = public override fun parse(raw: String, sender: CommandSender): Float =
raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数") raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数")
} }
@ -73,14 +75,14 @@ public object FloatArgumentParser : InternalCommandArgumentParserExtensions<Floa
/** /**
* 直接返回 [String], 或取用 [SingleMessage.contentToString] * 直接返回 [String], 或取用 [SingleMessage.contentToString]
*/ */
public object StringArgumentParser : InternalCommandArgumentParserExtensions<String> { public object StringValueArgumentParser : InternalCommandValueArgumentParserExtensions<String>() {
public override fun parse(raw: String, sender: CommandSender): String = raw public override fun parse(raw: String, sender: CommandSender): String = raw
} }
/** /**
* 解析 [String] 通过 [Image]. * 解析 [String] 通过 [Image].
*/ */
public object ImageArgumentParser : InternalCommandArgumentParserExtensions<Image> { public object ImageValueArgumentParser : InternalCommandValueArgumentParserExtensions<Image>() {
public override fun parse(raw: String, sender: CommandSender): Image { public override fun parse(raw: String, sender: CommandSender): Image {
return kotlin.runCatching { return kotlin.runCatching {
Image(raw) Image(raw)
@ -95,7 +97,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 { public override fun parse(raw: String, sender: CommandSender): PlainText {
return PlainText(raw) return PlainText(raw)
} }
@ -109,7 +111,7 @@ public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions<
/** /**
* 当字符串内容为(不区分大小写) "true", "yes", "enabled" * 当字符串内容为(不区分大小写) "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 -> public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true) str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true) || str.equals("yes", ignoreCase = true)
@ -121,7 +123,7 @@ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Bo
/** /**
* 根据 [Bot.id] 解析一个登录后的 [Bot] * 根据 [Bot.id] 解析一个登录后的 [Bot]
*/ */
public object ExistingBotArgumentParser : InternalCommandArgumentParserExtensions<Bot> { public object ExistingBotValueArgumentParser : InternalCommandValueArgumentParserExtensions<Bot>() {
public override fun parse(raw: String, sender: CommandSender): Bot = public override fun parse(raw: String, sender: CommandSender): Bot =
if (raw == "~") sender.inferBotOrFail() if (raw == "~") sender.inferBotOrFail()
else raw.findBotOrFail() else raw.findBotOrFail()
@ -136,7 +138,7 @@ public object ExistingBotArgumentParser : InternalCommandArgumentParserExtension
/** /**
* 解析任意一个存在的好友. * 解析任意一个存在的好友.
*/ */
public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions<Friend> { public object ExistingFriendValueArgumentParser : InternalCommandValueArgumentParserExtensions<Friend>() {
private val syntax = """ private val syntax = """
- `botId.friendId` - `botId.friendId`
- `botId.friendNick` (模糊搜索, 寻找最优匹配) - `botId.friendNick` (模糊搜索, 寻找最优匹配)
@ -175,7 +177,7 @@ public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtens
/** /**
* 解析任意一个存在的群. * 解析任意一个存在的群.
*/ */
public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions<Group> { public object ExistingGroupValueArgumentParser : InternalCommandValueArgumentParserExtensions<Group>() {
private val syntax = """ private val syntax = """
- `botId.groupId` - `botId.groupId`
- `~` (指代指令调用人自己所在群. 仅群聊天环境下) - `~` (指代指令调用人自己所在群. 仅群聊天环境下)
@ -202,7 +204,7 @@ public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensi
} }
} }
public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensions<User> { public object ExistingUserValueArgumentParser : InternalCommandValueArgumentParserExtensions<User>() {
private val syntax: String = """ private val syntax: String = """
- `botId.groupId.memberId` - `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -215,11 +217,11 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
""".trimIndent() """.trimIndent()
override fun parse(raw: String, sender: CommandSender): User { 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 { 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( private fun <T> parseImpl(
@ -246,7 +248,7 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
} }
public object ExistingContactArgumentParser : InternalCommandArgumentParserExtensions<Contact> { public object ExistingContactValueArgumentParser : InternalCommandValueArgumentParserExtensions<Contact>() {
private val syntax: String = """ private val syntax: String = """
- `botId.groupId.memberId` - `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -259,11 +261,11 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
""".trimIndent() """.trimIndent()
override fun parse(raw: String, sender: CommandSender): Contact { 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 { 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( private fun <T> parseImpl(
@ -286,7 +288,7 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
/** /**
* 解析任意一个群成员. * 解析任意一个群成员.
*/ */
public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtensions<Member> { public object ExistingMemberValueArgumentParser : InternalCommandValueArgumentParserExtensions<Member>() {
private val syntax: String = """ private val syntax: String = """
- `botId.groupId.memberId` - `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配) - `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -333,7 +335,7 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens
} }
} }
public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> { public object PermissionIdValueArgumentParser : InternalCommandValueArgumentParserExtensions<PermissionId>() {
override fun parse(raw: String, sender: CommandSender): PermissionId { override fun parse(raw: String, sender: CommandSender): PermissionId {
return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse { return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse {
illegalArgument("无法解析 $raw 为 PermissionId") illegalArgument("无法解析 $raw 为 PermissionId")
@ -341,7 +343,7 @@ public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> {
} }
} }
public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> { public object PermitteeIdValueArgumentParser : InternalCommandValueArgumentParserExtensions<PermitteeId>() {
override fun parse(raw: String, sender: CommandSender): PermitteeId { override fun parse(raw: String, sender: CommandSender): PermitteeId {
return if (raw == "~") sender.permitteeId return if (raw == "~") sender.permitteeId
else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse { else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse {
@ -351,32 +353,38 @@ public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> {
override fun parse(raw: MessageContent, sender: CommandSender): PermitteeId { override fun parse(raw: MessageContent, sender: CommandSender): PermitteeId {
if (raw is At) { 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) return super.parse(raw, sender)
} }
} }
internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> { /** 直接返回原始参数 [MessageContent] */
fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") public object RawContentValueArgumentParser : CommandValueArgumentParser<MessageContent> {
override fun parse(raw: String, sender: CommandSender): MessageContent = PlainText(raw)
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
}
fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this") internal abstract class InternalCommandValueArgumentParserExtensions<T : Any> : AbstractCommandValueArgumentParser<T>() {
private fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
fun String.findBotOrFail(): Bot = protected fun Long.findBotOrFail(): Bot = Bot.getInstanceOrNull(this) ?: illegalArgument("无法找到 Bot: $this")
protected fun String.findBotOrFail(): Bot =
Bot.getInstanceOrNull(this.parseToLongOrFail()) ?: illegalArgument("无法找到 Bot: $this") Bot.getInstanceOrNull(this.parseToLongOrFail()) ?: illegalArgument("无法找到 Bot: $this")
fun Bot.findGroupOrFail(id: Long): Group = getGroupOrNull(id) ?: illegalArgument("无法找到群: $this") protected fun Bot.findGroupOrFail(id: Long): Group = getGroupOrNull(id) ?: illegalArgument("无法找到群: $this")
fun Bot.findGroupOrFail(id: String): Group = protected fun Bot.findGroupOrFail(id: String): Group =
getGroupOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群: $this") getGroupOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群: $this")
fun Bot.findFriendOrFail(id: String): Friend = protected fun Bot.findFriendOrFail(id: String): Friend =
getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到好友: $this") getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到好友: $this")
fun Bot.findMemberOrFail(id: String): Friend = protected fun Bot.findMemberOrFail(id: String): Friend =
getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群员: $this") getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群员: $this")
fun Group.findMemberOrFail(idOrCard: String): Member { protected fun Group.findMemberOrFail(idOrCard: String): Member {
if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员") if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员")
idOrCard.toLongOrNull()?.let { getOrNull(it) }?.let { return it } idOrCard.toLongOrNull()?.let { getOrNull(it) }?.let { return it }
this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard) }?.let { return it } this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard) }?.let { return it }
@ -399,23 +407,21 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg
} }
} }
fun CommandSender.inferBotOrFail(): Bot = protected fun CommandSender.inferBotOrFail(): Bot =
(this as? UserCommandSender)?.bot (this as? UserCommandSender)?.bot
?: Bot.botInstancesSequence.singleOrNull() ?: Bot.botInstancesSequence.singleOrNull()
?: illegalArgument("当前语境下无法推断目标 Bot, 因为目前有多个 Bot 在线.") ?: illegalArgument("当前语境下无法推断目标 Bot, 因为目前有多个 Bot 在线.")
fun CommandSender.inferGroupOrFail(): Group = protected fun CommandSender.inferGroupOrFail(): Group =
inferGroup() ?: illegalArgument("当前语境下无法推断目标群") inferGroup() ?: illegalArgument("当前语境下无法推断目标群")
fun CommandSender.inferGroup(): Group? = (this as? GroupAwareCommandSender)?.group protected fun CommandSender.inferGroup(): Group? = (this as? GroupAwareCommandSender)?.group
fun CommandSender.inferFriendOrFail(): Friend = protected fun CommandSender.inferFriendOrFail(): Friend =
(this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友") (this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友")
} }
internal fun Double.toDecimalPlace(n: Int): String { internal fun Double.toDecimalPlace(n: Int): String = "%.${n}f".format(this)
return "%.${n}f".format(this)
}
internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString { internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString {
var lengthSum = 0 var lengthSum = 0

View File

@ -0,0 +1,307 @@
/*
* 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 AbstractCommandSignature
*/
@ExperimentalCommandDescriptors
public interface CommandSignature {
/**
* 接收者参数, [CommandSender] 子类
*/
@ConsoleExperimentalApi
public val receiverParameter: CommandReceiverParameter<out CommandSender>?
/**
* 形式 值参数.
*/
public val valueParameters: List<AbstractCommandValueParameter<*>>
/**
* 调用这个指令.
*/
public suspend fun call(resolvedCommandCall: ResolvedCommandCall)
}
/**
* 来自 [KFunction] 反射得到的 [CommandSignature]
*
* @see CommandSignatureFromKFunctionImpl
*/
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public interface CommandSignatureFromKFunction : CommandSignature {
public val originFunction: KFunction<*>
}
/**
* @see CommandSignatureImpl
* @see CommandSignatureFromKFunctionImpl
*/
@ExperimentalCommandDescriptors
public abstract class AbstractCommandSignature : CommandSignature {
override fun toString(): String {
val receiverParameter = receiverParameter
return if (receiverParameter == null) {
"CommandSignatureVariant(${valueParameters.joinToString()})"
} else {
"CommandSignatureVariant($receiverParameter, ${valueParameters.joinToString()})"
}
}
}
@ExperimentalCommandDescriptors
public open class CommandSignatureImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
private val onCall: suspend CommandSignatureImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignature, AbstractCommandSignature() {
override suspend fun call(resolvedCommandCall: ResolvedCommandCall) {
return onCall(resolvedCommandCall)
}
}
@ConsoleExperimentalApi
@ExperimentalCommandDescriptors
public open class CommandSignatureFromKFunctionImpl(
override val receiverParameter: CommandReceiverParameter<out CommandSender>?,
override val valueParameters: List<AbstractCommandValueParameter<*>>,
override val originFunction: KFunction<*>,
private val onCall: suspend CommandSignatureFromKFunctionImpl.(resolvedCommandCall: ResolvedCommandCall) -> Unit,
) : CommandSignatureFromKFunction, AbstractCommandSignature() {
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

@ -0,0 +1,152 @@
/*
* 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", "unused")
package net.mamoe.mirai.console.command.descriptor
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.console.command.descriptor.CommandValueArgumentParser.Companion.parse
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* 指令参数解析器. 用于解析字符串或 [SingleMessage] 到特定参数类型.
*
* ### 参数解析
*
* [SimpleCommand] 中的示例:
* ```
* suspend fun CommandSender.mute(target: Member, duration: Int)
* ```
* [CommandManager] 总是从 [SimpleCommand.context] 搜索一个 [T] [Member] [CommandValueArgumentParser], 并调用其 [CommandValueArgumentParser.parse]
*
* ### 内建指令解析器
* - 基础类型: [ByteValueArgumentParser], [ShortValueArgumentParser], [IntValueArgumentParser], [LongValueArgumentParser]
* [FloatValueArgumentParser], [DoubleValueArgumentParser],
* [BooleanValueArgumentParser], [StringValueArgumentParser]
*
* - [Bot]: [ExistingBotValueArgumentParser]
* - [Friend]: [ExistingFriendValueArgumentParser]
* - [Group]: [ExistingGroupValueArgumentParser]
* - [Member]: [ExistingMemberValueArgumentParser]
* - [User]: [ExistingUserValueArgumentParser]
* - [Contact]: [ExistingContactValueArgumentParser]
*
*
* @see SimpleCommand 简单指令
* @see CompositeCommand 复合指令
*
* @see buildCommandArgumentContext 指令参数环境, [CommandValueArgumentParser] 的集合
*/
public interface CommandValueArgumentParser<out T : Any> {
/**
* 解析一个字符串为 [T] 类型参数
*
* **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException].
* 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户.
*
* @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出.
*
* @see CommandArgumentParserException
*/
@Throws(CommandArgumentParserException::class)
public fun parse(raw: String, sender: CommandSender): T
/**
* 解析一个消息内容元素为 [T] 类型参数
*
* **实现提示**: 在解析时遇到意料之中的问题, 如无法找到目标群员, 可抛出 [CommandArgumentParserException].
* 此异常将会被特殊处理, 不会引发一个错误, 而是作为指令调用成功的情况, 将错误信息发送给用户.
*
* @throws CommandArgumentParserException 当解析时遇到*意料之中*的问题时抛出.
*
* @see CommandArgumentParserException
*/
@Throws(CommandArgumentParserException::class)
public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender)
public companion object {
/**
* 解析一个字符串或 [SingleMessage] [T] 类型参数
*
* @throws IllegalArgumentException [raw] 既不是 [SingleMessage], 也不是 [String] 时抛出.
*
* @see CommandValueArgumentParser.parse
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun <T : Any> CommandValueArgumentParser<T>.parse(raw: Message, sender: CommandSender): T {
return when (raw) {
is PlainText -> parse(raw.content, sender)
is MessageContent -> parse(raw, sender)
else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}")
}
}
/**
* 使用原 [this] 解析, 成功后使用 [mapper] 映射为另一个类型.
*/
@JvmStatic
public fun <Original : Any, Result : Any> CommandValueArgumentParser<Original>.map(
mapper: MappingCommandValueArgumentParser<Original, Result>.(Original) -> Result,
): CommandValueArgumentParser<Result> = MappingCommandValueArgumentParser(this, mapper)
}
}
/**
* @see CommandValueArgumentParser 的基础实现.
*/
public abstract class AbstractCommandValueArgumentParser<T : Any> : CommandValueArgumentParser<T> {
public companion object {
/**
* 抛出一个 [CommandArgumentParserException] 的捷径
*
* @throws CommandArgumentParserException
*/
@JvmStatic
@JvmSynthetic
@Throws(CommandArgumentParserException::class)
protected inline fun CommandValueArgumentParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing =
throw CommandArgumentParserException(message, cause)
/**
* 检查参数 [condition]. 当它为 `false` 时调用 [message] 并以其返回值作为消息, 抛出异常 [CommandArgumentParserException]
*
* @throws CommandArgumentParserException
*/
@JvmStatic
@Throws(CommandArgumentParserException::class)
@JvmSynthetic
protected inline fun CommandValueArgumentParser<*>.checkArgument(
condition: Boolean,
crossinline message: () -> String = { "Check failed." },
) {
contract {
returns() implies condition
callsInPlace(message, InvocationKind.AT_MOST_ONCE)
}
if (!condition) illegalArgument(message())
}
}
}
public class MappingCommandValueArgumentParser<T : Any, R : Any>(
private val original: CommandValueArgumentParser<T>,
private val mapper: MappingCommandValueArgumentParser<T, R>.(T) -> R,
) : AbstractCommandValueArgumentParser<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))
}

View File

@ -0,0 +1,59 @@
/*
* 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.Command
import net.mamoe.mirai.console.command.IllegalCommandArgumentException
import net.mamoe.mirai.console.command.descriptor.AbstractCommandValueArgumentParser.Companion.illegalArgument
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 CommandDeclarationClashException(
public val command: Command,
public val signatures: List<CommandSignature>,
) : CommandResolutionException("Command declaration clash: \n${signatures.joinToString("\n")}")
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)
}
/**
* 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范等.
*
* [message] 将会发送给指令调用方.
*
* @see IllegalCommandArgumentException
* @see CommandValueArgumentParser
* @see AbstractCommandValueArgumentParser.illegalArgument
*/
public class CommandArgumentParserException : IllegalCommandArgumentException {
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

@ -1,40 +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.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
/**
* Java 用户添加协程帮助的 [Command].
*
* 注意, [JSimpleCommand], [JCompositeCommand], [JRawCommand] 都不实现这个接口. [JCommand] 只设计为 Java 使用者自己实现 [Command] 相关内容.
*
* @see Command
*/
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
}

View File

@ -13,7 +13,8 @@ import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.description.buildCommandArgumentContext import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.descriptor.buildCommandArgumentContext
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
@ -83,6 +84,7 @@ public abstract class JCompositeCommand
protected set protected set
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@ExperimentalCommandDescriptors
public final override var prefixOptional: Boolean = false public final override var prefixOptional: Boolean = false
protected set protected set

View File

@ -9,16 +9,15 @@
package net.mamoe.mirai.console.command.java package net.mamoe.mirai.console.command.java
import kotlinx.coroutines.Dispatchers import net.mamoe.mirai.console.command.BuiltInCommands
import kotlinx.coroutines.withContext import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME 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.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
/** /**
* Java 用户继承 * Java 用户继承
@ -70,21 +69,7 @@ public abstract class JRawCommand
protected set protected set
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@ExperimentalCommandDescriptors
public final override var prefixOptional: Boolean = false public final override var prefixOptional: Boolean = false
protected set 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

@ -10,10 +10,10 @@
package net.mamoe.mirai.console.command.java package net.mamoe.mirai.console.command.java
import net.mamoe.mirai.console.command.CommandManager 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.CommandOwner
import net.mamoe.mirai.console.command.SimpleCommand 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.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
@ -50,9 +50,10 @@ public abstract class JSimpleCommand(
) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) { ) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) {
public override var description: String = super.description public override var description: String = super.description
protected set protected set
public override var permission: Permission = super.permission public override var permission: Permission = super.permission
protected set protected set
@ExperimentalCommandDescriptors
public override var prefixOptional: Boolean = super.prefixOptional public override var prefixOptional: Boolean = super.prefixOptional
protected set protected set
public override var context: CommandArgumentContext = super.context public override var context: CommandArgumentContext = super.context

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,27 @@
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.extensions.CommandCallParserProviderImpl
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 by CommandCallParserProviderImpl(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.signature,
signature.zippedArguments.map { it.second },
context ?: EmptyCommandArgumentContext)
}
private data class ResolveData(
val signature: CommandSignature,
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(
signature = signature,
zippedArguments = emptyList(),
argumentAcceptances = emptyList(),
remainingParameters = remainingParameters,
)
} else {
if (valueArguments.size > valueParameters.size && zipped.last().first.isVararg) {
// merge vararg arguments
val (varargParameter, _)
= 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(
signature = 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.signature.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,88 @@
/*
* 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.descriptor.CommandValueArgumentParser.Companion.parse
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 [CommandSignature], specifically a sub command from [CompositeCommand]
*/
public val calleeSignature: CommandSignature
/**
* Original arguments
*/
public val rawValueArguments: List<CommandValueArgument>
/**
* Resolved value arguments arranged mapping the [CommandSignature.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: CommandSignature,
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

@ -11,22 +11,21 @@
package net.mamoe.mirai.console.compiler.common package net.mamoe.mirai.console.compiler.common
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.data.value
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.util.SemVersion
import kotlin.annotation.AnnotationTarget.* import kotlin.annotation.AnnotationTarget.*
/** /**
* 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断. * 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断.
*/ */
@ConsoleExperimentalApi @Target(VALUE_PARAMETER, PROPERTY, FIELD, FUNCTION, TYPE, TYPE_PARAMETER)
@Target(
VALUE_PARAMETER,
PROPERTY, FIELD,
FUNCTION,
TYPE, TYPE_PARAMETER
)
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
public annotation class ResolveContext( public annotation class ResolveContext(
val kind: Kind, vararg val kinds: Kind,
) { ) {
/** /**
* 元素数量可能在任意时间被改动 * 元素数量可能在任意时间被改动
@ -36,18 +35,57 @@ public annotation class ResolveContext(
// ConstantKind // ConstantKind
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION /*
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION * WARNING: IF YOU CHANGE NAMES HERE,
PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION * YOU SHOULD ALSO CHANGE THEIR COUNTERPARTS AT net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind
*/
/**
* @see PluginDescription.id
*/
PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION
/**
* @see PluginDescription.name
*/
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION
/**
* @see PluginDescription.version
* @see SemVersion.Companion.invoke
*/
SEMANTIC_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION
/**
* @see SemVersion.Companion.parseRangeRequirement
*/
VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT // TODO VERSION_REQUIREMENT, // ILLEGAL_VERSION_REQUIREMENT // TODO
/**
* @see Command.allNames
*/
COMMAND_NAME, // ILLEGAL_COMMAND_NAME COMMAND_NAME, // ILLEGAL_COMMAND_NAME
PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE /**
PERMISSION_NAME, // ILLEGAL_COMMAND_NAME * @see PermissionId.name
PERMISSION_ID, // ILLEGAL_COMMAND_ID */
PERMISSION_NAMESPACE, // ILLEGAL_PERMISSION_NAMESPACE
/**
* @see PermissionId.name
*/
PERMISSION_NAME, // ILLEGAL_PERMISSION_NAME
/**
* @see PermissionId.parseFromString
*/
PERMISSION_ID, // ILLEGAL_PERMISSION_ID
/**
* 标注一个泛型, 要求这个泛型必须拥有一个公开无参 (或所有参数都可选) 构造器.
*
* @see PluginData.value
*/
RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE
} }
} }

View File

@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole 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.internal.plugin.updateWhen
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*

View File

@ -9,6 +9,8 @@
package net.mamoe.mirai.console.data package net.mamoe.mirai.console.data
import kotlinx.serialization.SerialInfo
/** /**
* 序列化之后的注释. * 序列化之后的注释.
* *
@ -30,6 +32,7 @@ package net.mamoe.mirai.console.data
* a: b * a: b
* ``` * ```
*/ */
@SerialInfo
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
public annotation class ValueDescription(val value: String) public annotation class ValueDescription(val value: String)

View File

@ -7,24 +7,72 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.extension package net.mamoe.mirai.console.extension
import net.mamoe.mirai.console.extensions.SingletonExtensionSelector
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* [Extension] `companion` 实现. * [Extension] 的伴生对象实现.
*
* @see AbstractExtensionPoint
*/ */
public interface ExtensionPoint<T : Extension> { public interface ExtensionPoint<T : Extension> {
/**
* 扩展实例 [T] 的类型
*/
public val extensionType: KClass<T> public val extensionType: KClass<T>
} }
public open class AbstractExtensionPoint<T : Extension>( public abstract class AbstractExtensionPoint<T : Extension>(
public override val extensionType: KClass<T>, public override val extensionType: KClass<T>,
) : ExtensionPoint<T> ) : ExtensionPoint<T>
/** /**
* 表示一个 [SingletonExtension] [ExtensionPoint] * 表示一个 [SingletonExtension] [ExtensionPoint]
*/ */
public interface SingletonExtensionPoint<T : SingletonExtension<*>> : ExtensionPoint<T> public interface SingletonExtensionPoint<T : SingletonExtension<*>> : ExtensionPoint<T>
/**
* 表示一个 [InstanceExtension] [ExtensionPoint]
*/
public interface InstanceExtensionPoint<T : InstanceExtension<*>> : ExtensionPoint<T>
/**
* 表示一个 [FunctionExtension] [ExtensionPoint]
*/
public interface FunctionExtensionPoint<T : FunctionExtension> : ExtensionPoint<T>
public abstract class AbstractInstanceExtensionPoint<E : InstanceExtension<T>, T>(
extensionType: KClass<E>,
/**
* 内建的实现列表.
*/
@ConsoleExperimentalApi
public vararg val builtinImplementations: E,
) : AbstractExtensionPoint<E>(extensionType)
public abstract class AbstractSingletonExtensionPoint<E : SingletonExtension<T>, T>(
extensionType: KClass<E>,
/**
* 内建的实现.
*/
@ConsoleExperimentalApi
public val builtinImplementation: T,
) : AbstractExtensionPoint<E>(extensionType), SingletonExtensionPoint<E> {
/**
* [SingletonExtensionSelector] 选择后的实例.
*/
@ConsoleExperimentalApi
public val selectedInstance: T by lazy {
GlobalComponentStorage.run { this@AbstractSingletonExtensionPoint.findSingletonInstance(extensionType, builtinImplementation) }
}
}

View File

@ -9,6 +9,8 @@
package net.mamoe.mirai.console.extension package net.mamoe.mirai.console.extension
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCallParser
import net.mamoe.mirai.console.extensions.* import net.mamoe.mirai.console.extensions.*
import net.mamoe.mirai.console.internal.extension.AbstractConcurrentComponentStorage import net.mamoe.mirai.console.internal.extension.AbstractConcurrentComponentStorage
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
@ -35,7 +37,7 @@ public class PluginComponentStorage(
): Unit = contribute(extensionPoint, plugin, lazyInstance) ): Unit = contribute(extensionPoint, plugin, lazyInstance)
/** /**
* 注册一个扩展 * 注册一个扩展. [E] 必须拥有伴生对象为 [ExtensionPoint].
*/ */
public inline fun <reified E : Extension> contribute( public inline fun <reified E : Extension> contribute(
noinline lazyInstance: () -> E, noinline lazyInstance: () -> E,
@ -56,6 +58,7 @@ public class PluginComponentStorage(
public fun contributeSingletonExtensionSelector(lazyInstance: () -> SingletonExtensionSelector): Unit = public fun contributeSingletonExtensionSelector(lazyInstance: () -> SingletonExtensionSelector): Unit =
contribute(SingletonExtensionSelector, plugin, lazyInstance) contribute(SingletonExtensionSelector, plugin, lazyInstance)
@Suppress("SpellCheckingInspection") // alterer
/** 注册一个 [BotConfigurationAlterer] */ /** 注册一个 [BotConfigurationAlterer] */
public fun contributeBotConfigurationAlterer(instance: BotConfigurationAlterer): Unit = public fun contributeBotConfigurationAlterer(instance: BotConfigurationAlterer): Unit =
contribute(BotConfigurationAlterer, plugin, lazyInstance = { instance }) contribute(BotConfigurationAlterer, plugin, lazyInstance = { instance })
@ -73,16 +76,14 @@ public class PluginComponentStorage(
/** 注册一个 [PermissionServiceProvider] */ /** 注册一个 [PermissionServiceProvider] */
@OverloadResolutionByLambdaReturnType @OverloadResolutionByLambdaReturnType
public fun contributePermissionService( public fun contributePermissionService(lazyInstance: () -> PermissionService<*>): Unit =
lazyInstance: () -> PermissionService<*>, contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance))
): Unit = contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance))
/** 注册一个 [PermissionServiceProvider] */ /** 注册一个 [PermissionServiceProvider] */
@JvmName("contributePermissionServiceProvider") @JvmName("contributePermissionServiceProvider")
@OverloadResolutionByLambdaReturnType @OverloadResolutionByLambdaReturnType
public fun contributePermissionService( public fun contributePermissionService(lazyProvider: () -> PermissionServiceProvider): Unit =
lazyProvider: () -> PermissionServiceProvider, contribute(PermissionServiceProvider, plugin, lazyProvider)
): Unit = contribute(PermissionServiceProvider, plugin, lazyProvider)
///////////////////////////////////// /////////////////////////////////////
@ -95,5 +96,20 @@ public class PluginComponentStorage(
@JvmName("contributePluginLoaderProvider") @JvmName("contributePluginLoaderProvider")
@OverloadResolutionByLambdaReturnType @OverloadResolutionByLambdaReturnType
public fun contributePluginLoader(lazyProvider: () -> PluginLoaderProvider): Unit = public fun contributePluginLoader(lazyProvider: () -> PluginLoaderProvider): Unit =
contribute(PluginLoaderProvider, plugin, lazyProvider) contribute(PluginLoaderProvider, plugin, lazyProvider) // lazy for safety
/////////////////////////////////////
/** 注册一个 [CommandCallParserProvider] */
@ExperimentalCommandDescriptors
@OverloadResolutionByLambdaReturnType
public fun contributeCommandCallParser(lazyInstance: () -> CommandCallParser): Unit =
contribute(CommandCallParserProvider, plugin, LazyCommandCallParserProviderImpl(lazyInstance))
/** 注册一个 [CommandCallParserProvider] */
@ExperimentalCommandDescriptors
@JvmName("contributeCommandCallParserProvider")
@OverloadResolutionByLambdaReturnType
public fun contributeCommandCallParser(provider: CommandCallParserProvider): Unit =
contribute(CommandCallParserProvider, plugin, provider)
} }

View File

@ -21,6 +21,7 @@ import net.mamoe.mirai.utils.BotConfiguration
* *
* @see MiraiConsole.addBot * @see MiraiConsole.addBot
*/ */
@Suppress("SpellCheckingInspection") // alterer
public fun interface BotConfigurationAlterer : FunctionExtension { public fun interface BotConfigurationAlterer : FunctionExtension {
/** /**

View File

@ -0,0 +1,35 @@
/*
* 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.AbstractInstanceExtensionPoint
import net.mamoe.mirai.console.extension.InstanceExtension
/**
* The provider of [CommandCallParser]
*/
@ExperimentalCommandDescriptors
public interface CommandCallParserProvider : InstanceExtension<CommandCallParser> {
public companion object ExtensionPoint :
AbstractInstanceExtensionPoint<CommandCallParserProvider, CommandCallParser>(CommandCallParserProvider::class,
SpaceSeparatedCommandCallParser.Provider)
}
@ExperimentalCommandDescriptors
public class CommandCallParserProviderImpl(override val instance: CommandCallParser) : CommandCallParserProvider
@ExperimentalCommandDescriptors
public class LazyCommandCallParserProviderImpl(instanceCalculator: () -> CommandCallParser) : CommandCallParserProvider {
override val instance: CommandCallParser by lazy(instanceCalculator)
}

View File

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

View File

@ -9,9 +9,9 @@
package net.mamoe.mirai.console.extensions package net.mamoe.mirai.console.extensions
import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.AbstractSingletonExtensionPoint
import net.mamoe.mirai.console.extension.SingletonExtension import net.mamoe.mirai.console.extension.SingletonExtension
import net.mamoe.mirai.console.extension.SingletonExtensionPoint import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
/** /**
@ -21,8 +21,7 @@ import net.mamoe.mirai.console.permission.PermissionService
*/ */
public interface PermissionServiceProvider : SingletonExtension<PermissionService<*>> { public interface PermissionServiceProvider : SingletonExtension<PermissionService<*>> {
public companion object ExtensionPoint : public companion object ExtensionPoint :
AbstractExtensionPoint<PermissionServiceProvider>(PermissionServiceProvider::class), AbstractSingletonExtensionPoint<PermissionServiceProvider, PermissionService<*>>(PermissionServiceProvider::class, BuiltInPermissionService)
SingletonExtensionPoint<PermissionServiceProvider>
} }
/** /**

View File

@ -12,12 +12,19 @@ package net.mamoe.mirai.console.extensions
import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.AbstractExtensionPoint
import net.mamoe.mirai.console.extension.Extension import net.mamoe.mirai.console.extension.Extension
import net.mamoe.mirai.console.extension.InstanceExtension import net.mamoe.mirai.console.extension.InstanceExtension
import net.mamoe.mirai.console.extension.PluginComponentStorage
import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader
/** /**
* 提供扩展 [PluginLoader] * 提供扩展 [PluginLoader]
* *
* @see PluginComponentStorage.contributePluginLoader
*
*
* @see Extension * @see Extension
* @see PluginLoader
*
* @see LazyPluginLoaderProviderImpl
*/ */
public interface PluginLoaderProvider : InstanceExtension<PluginLoader<*, *>> { public interface PluginLoaderProvider : InstanceExtension<PluginLoader<*, *>> {
public companion object ExtensionPoint : AbstractExtensionPoint<PluginLoaderProvider>(PluginLoaderProvider::class) public companion object ExtensionPoint : AbstractExtensionPoint<PluginLoaderProvider>(PluginLoaderProvider::class)

View File

@ -9,8 +9,11 @@
package net.mamoe.mirai.console.extensions package net.mamoe.mirai.console.extensions
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.AbstractExtensionPoint
import net.mamoe.mirai.console.extension.ExtensionException
import net.mamoe.mirai.console.extension.FunctionExtension import net.mamoe.mirai.console.extension.FunctionExtension
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
/** /**
* Console 启动完成后立即在主线程调用的扩展. 用于进行一些必要的延迟初始化. * Console 启动完成后立即在主线程调用的扩展. 用于进行一些必要的延迟初始化.
@ -20,7 +23,13 @@ import net.mamoe.mirai.console.extension.FunctionExtension
public fun interface PostStartupExtension : FunctionExtension { public fun interface PostStartupExtension : FunctionExtension {
/** /**
* 将在 Console 主线程执行. * 将在 Console 主线程执行.
*
* @throws Exception 所有抛出的 [Exception] 都会被捕获并包装为 [ExtensionException] 抛出, 并停止 [MiraiConsole]
*
* #### 内部实现细节
* [MiraiConsoleImplementationBridge.doStart] 所有 [MiraiConsoleImplementationBridge.phase] 执行完成后顺序调用.
*/ */
@Throws(Exception::class)
public operator fun invoke() public operator fun invoke()
public companion object ExtensionPoint : AbstractExtensionPoint<PostStartupExtension>(PostStartupExtension::class) public companion object ExtensionPoint : AbstractExtensionPoint<PostStartupExtension>(PostStartupExtension::class)

View File

@ -29,7 +29,7 @@ import kotlin.reflect.KClass
*/ */
public interface SingletonExtensionSelector : FunctionExtension { public interface SingletonExtensionSelector : FunctionExtension {
public data class Registry<T : Extension>( public data class Registry<T : Extension>(
val plugin: Plugin, val plugin: Plugin?,
val extension: T, val extension: T,
) )
@ -55,11 +55,11 @@ public interface SingletonExtensionSelector : FunctionExtension {
instances.isEmpty() -> BuiltInSingletonExtensionSelector instances.isEmpty() -> BuiltInSingletonExtensionSelector
instances.size == 1 -> { instances.size == 1 -> {
instances.single().also { (plugin, ext) -> 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 }.extension
} }
else -> { 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

@ -41,7 +41,7 @@ import net.mamoe.mirai.console.internal.util.autoHexToBytes
import net.mamoe.mirai.console.logging.* import net.mamoe.mirai.console.logging.*
import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission import net.mamoe.mirai.console.permission.PermissionService.Companion.permit
import net.mamoe.mirai.console.permission.RootPermission import net.mamoe.mirai.console.permission.RootPermission
import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.center.PluginCenter import net.mamoe.mirai.console.plugin.center.PluginCenter
@ -177,9 +177,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
phase `load PermissionService`@{ phase `load PermissionService`@{
mainLogger.verbose { "Loading PermissionService..." } mainLogger.verbose { "Loading PermissionService..." }
PermissionService.instanceField = GlobalComponentStorage.run { PermissionServiceProvider.selectedInstance // init
PermissionServiceProvider.findSingletonInstance(BuiltInPermissionService)
}
PermissionService.INSTANCE.let { ps -> PermissionService.INSTANCE.let { ps ->
if (ps is BuiltInPermissionService) { if (ps is BuiltInPermissionService) {
@ -188,7 +186,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
} }
} }
ConsoleCommandSender.grantPermission(RootPermission) ConsoleCommandSender.permit(RootPermission)
} }
phase `prepare commands`@{ phase `prepare commands`@{
@ -231,7 +229,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
} }
GlobalComponentStorage.run { GlobalComponentStorage.run {
PostStartupExtension.useExtensions { it() } PostStartupExtension.useExtensions { it() } // exceptions thrown will be caught by caller of `doStart`.
} }
mainLogger.info { "mirai-console started successfully." } mainLogger.info { "mirai-console started successfully." }

View File

@ -15,18 +15,25 @@ import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.Command.Companion.allNames 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.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.Listener
import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.message.MessageEvent 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.Message
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.asMessageChain import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.message.data.content import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.locks.ReentrantLock 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 { private val logger: MiraiLogger by lazy {
MiraiConsole.createLogger("command") MiraiConsole.createLogger("command")
} }
@ -48,11 +55,11 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
/** /**
* 从原始的 command 中解析出 Command 对象 * 从原始的 command 中解析出 Command 对象
*/ */
internal fun matchCommand(rawCommand: String): Command? { override fun matchCommand(commandName: String): Command? {
if (rawCommand.startsWith(commandPrefix)) { if (commandName.startsWith(commandPrefix)) {
return requiredPrefixCommandMap[rawCommand.substringAfter(commandPrefix).toLowerCase()] return requiredPrefixCommandMap[commandName.substringAfter(commandPrefix).toLowerCase()]
} }
return optionalPrefixCommandMap[rawCommand.toLowerCase()] return optionalPrefixCommandMap[commandName.toLowerCase()]
} }
internal val commandListener: Listener<MessageEvent> by lazy { internal val commandListener: Listener<MessageEvent> by lazy {
@ -65,13 +72,17 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
) { ) {
val sender = this.toCommandSender() val sender = this.toCommandSender()
when (val result = sender.executeCommand(message)) { when (val result = executeCommand(sender, message)) {
is CommandExecuteResult.PermissionDenied -> { is CommandExecuteResult.PermissionDenied -> {
if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) { if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) {
sender.sendMessage("权限不足") sender.sendMessage("权限不足")
intercept() intercept()
} }
} }
is CommandExecuteResult.IllegalArgument -> {
result.exception.message?.let { sender.sendMessage(it) }
intercept()
}
is CommandExecuteResult.Success -> { is CommandExecuteResult.Success -> {
intercept() intercept()
} }
@ -79,7 +90,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
sender.catchExecutionException(result.exception) sender.catchExecutionException(result.exception)
intercept() intercept()
} }
is CommandExecuteResult.CommandNotFound -> { is CommandExecuteResult.UnresolvedCall -> {
// noop // noop
} }
} }
@ -90,102 +101,90 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
///// IMPL ///// 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 allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy
override val commandPrefix: String get() = "/" override val commandPrefix: String get() = "/"
override fun CommandOwner.unregisterAllCommands() { override fun unregisterAllCommands(owner: CommandOwner) {
for (registeredCommand in registeredCommands) { for (registeredCommand in getRegisteredCommands(owner)) {
registeredCommand.unregister() unregisterCommand(registeredCommand)
} }
} }
override fun Command.register(override: Boolean): Boolean { override fun registerCommand(command: Command, override: Boolean): Boolean {
if (this is CompositeCommand) this.subCommands // init lazy if (command is CompositeCommand) {
command.overloads // init lazy
}
kotlin.runCatching { kotlin.runCatching {
this.permission // init lazy command.permission // init lazy
this.secondaryNames // init lazy command.secondaryNames // init lazy
this.description // init lazy command.description // init lazy
this.usage // init lazy command.usage // init lazy
}.onFailure { }.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 (!override) {
if (findDuplicate() != null) return false if (command.findDuplicate() != null) return false
} }
_registeredCommands.add(this@register) this@CommandManagerImpl._registeredCommands.add(command)
if (this.prefixOptional) { if (command.prefixOptional) {
for (name in this.allNames) { for (name in command.allNames) {
val lowerCaseName = name.toLowerCase() val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap[lowerCaseName] = this this@CommandManagerImpl.optionalPrefixCommandMap[lowerCaseName] = command
requiredPrefixCommandMap[lowerCaseName] = this this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
} }
} else { } else {
for (name in this.allNames) { for (name in command.allNames) {
val lowerCaseName = name.toLowerCase() val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency this@CommandManagerImpl.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
requiredPrefixCommandMap[lowerCaseName] = this this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
} }
} }
return true return true
} }
} }
override fun Command.findDuplicate(): Command? = override fun findDuplicateCommand(command: Command): Command? =
_registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames } _registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase command.allNames }
override fun Command.unregister(): Boolean = modifyLock.withLock { override fun unregisterCommand(command: Command): Boolean = modifyLock.withLock {
if (this.prefixOptional) { if (command.prefixOptional) {
this.allNames.forEach { command.allNames.forEach {
optionalPrefixCommandMap.remove(it) optionalPrefixCommandMap.remove(it.toLowerCase())
} }
} }
this.allNames.forEach { command.allNames.forEach {
requiredPrefixCommandMap.remove(it) requiredPrefixCommandMap.remove(it.toLowerCase())
} }
_registeredCommands.remove(this) _registeredCommands.remove(command)
} }
override fun Command.isRegistered(): Boolean = this in _registeredCommands override fun isCommandRegistered(command: Command): Boolean = command in _registeredCommands
}
override suspend fun Command.execute(
sender: CommandSender, // Don't move into CommandManager, compilation error / VerifyError
arguments: Message, @OptIn(ExperimentalCommandDescriptors::class)
checkPermission: Boolean internal suspend fun executeCommandImpl(
): CommandExecuteResult { message: Message,
return sender.executeCommandInternal( caller: CommandSender,
this, checkPermission: Boolean,
arguments.flattenCommandComponents(), ): CommandExecuteResult {
primaryName, val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCall("")
checkPermission 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 Command.execute( return try {
sender: CommandSender, resolved.calleeSignature.call(resolved)
arguments: String, CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain)
checkPermission: Boolean } catch (e: Throwable) {
): CommandExecuteResult { CommandExecuteResult.ExecutionFailed(e, resolved.callee, call.calleeName, EmptyMessageChain)
return sender.executeCommandInternal(
this,
arguments.flattenCommandComponents(),
primaryName,
checkPermission
)
} }
}
override suspend fun CommandSender.executeCommand(
message: Message,
checkPermission: Boolean
): CommandExecuteResult {
val msg = message.asMessageChain().filterIsInstance<MessageContent>()
if (msg.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(msg, msg[0].content.substringBefore(' '), checkPermission)
}
override suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean): CommandExecuteResult {
if (message.isBlank()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(message, message.substringBefore(' '), checkPermission)
}
}

View File

@ -0,0 +1,271 @@
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.KType
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<CommandSignatureFromKFunction>): 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(signatures: List<CommandSignatureFromKFunctionImpl>) {
data class ErasedParameterInfo(
val index: Int,
val name: String?,
val type: KType, // ignore nullability
val additional: String?,
)
data class ErasedVariantInfo(
val receiver: ErasedParameterInfo?,
val valueParameters: List<ErasedParameterInfo>,
)
fun CommandParameter<*>.toErasedParameterInfo(index: Int): ErasedParameterInfo {
return ErasedParameterInfo(index,
this.name,
this.type.withNullability(false),
if (this is AbstractCommandValueParameter.StringConstant) this.expectingValue else null)
}
val candidates = signatures.map { variant ->
variant to ErasedVariantInfo(
variant.receiverParameter?.toErasedParameterInfo(0),
variant.valueParameters.mapIndexed { index, parameter -> parameter.toErasedParameterInfo(index) }
)
}
val groups = candidates.groupBy { it.second }
val clashes = groups.entries.find { (_, value) ->
value.size > 1
} ?: return
throw CommandDeclarationClashException(command, clashes.value.map { it.first })
}
@Throws(IllegalCommandDeclarationException::class)
fun findSubCommands(): List<CommandSignatureFromKFunctionImpl> {
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() }
CommandSignatureFromKFunctionImpl(
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

@ -1,58 +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("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
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
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
)
}
/**
* 指令形式参数.
* @see toCommandParam
*/
internal data class CommandParameter<T : Any>(
/**
* 参数名. 不允许重复.
*/
val name: String,
/**
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgumentParser] 解析.
*/
val type: KClass<T> // exact type
) {
constructor(name: String, type: KClass<T>, parser: CommandArgumentParser<T>) : this(name, type) {
this._overrideParser = parser
}
@Suppress("PropertyName")
@JvmField
internal var _overrideParser: CommandArgumentParser<T>? = null
/**
* 覆盖的 [CommandArgumentParser].
*
* 如果非 `null`, 将不会从 [CommandArgumentContext] 寻找 [CommandArgumentParser]
*/
val overrideParser: CommandArgumentParser<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

@ -18,13 +18,6 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
internal infix fun Array<String>.matchesBeginning(list: List<Any>): Boolean {
this.forEachIndexed { index, any ->
if (list[index] != any) return false
}
return true
}
internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): Boolean { internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): Boolean {
val max = this.size.coerceAtMost(other.size) val max = this.size.coerceAtMost(other.size)
for (i in 0 until max) { for (i in 0 until max) {

View File

@ -12,7 +12,6 @@ package net.mamoe.mirai.console.internal.data
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.data.* 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.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SilentLogger import net.mamoe.mirai.utils.SilentLogger
@ -74,7 +73,9 @@ internal open class MultiFilePluginDataStorageImpl(
public override fun store(holder: PluginDataHolder, instance: PluginData) { public override fun store(holder: PluginDataHolder, instance: PluginData) {
getPluginDataFile(holder, instance).writeText( getPluginDataFile(holder, instance).writeText(
kotlin.runCatching { kotlin.runCatching {
yaml.encodeToString(instance.updaterSerializer, Unit) yaml.encodeToString(instance.updaterSerializer, Unit).also {
yaml.decodeAnyFromString(it) // test yaml
}
}.recoverCatching { }.recoverCatching {
// Just use mainLogger for convenience. // Just use mainLogger for convenience.
MiraiConsole.mainLogger.warning( MiraiConsole.mainLogger.warning(

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.PluginData
import net.mamoe.mirai.console.data.ValueName import net.mamoe.mirai.console.data.ValueName
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import kotlin.reflect.*
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf 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") @Suppress("UNCHECKED_CAST")
internal inline fun <reified T : Any> KType.toKClass(): KClass<out T> { internal inline fun <reified T : Any> KType.toKClass(): KClass<out T> {
val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" } val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" }
@ -41,8 +42,8 @@ internal inline fun <reified T : PluginData> newPluginDataInstanceUsingReflectio
?: createInstanceOrNull() ?: createInstanceOrNull()
?: throw IllegalArgumentException( ?: throw IllegalArgumentException(
"Cannot create PluginData instance. " + "Cannot create PluginData instance. " +
"PluginDataHolder supports PluginData implemented as an object " + "PluginDataHolder supports PluginData implemented as an object " +
"or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newPluginDataInstance implementation." "or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newPluginDataInstance implementation."
) )
} }
} }
@ -54,6 +55,12 @@ internal fun KType.classifierAsKClass() = when (val t = classifier) {
else -> error("Only KClass supported as classifier, got $t") else -> error("Only KClass supported as classifier, got $t")
} as KClass<Any> } as KClass<Any>
@Suppress("UNCHECKED_CAST")
internal fun KType.classifierAsKClassOrNull() = when (val t = classifier) {
is KClass<*> -> t
else -> null
} as KClass<Any>?
@JvmSynthetic @JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? { internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) } 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.SerializableValue.Companion.serializableValueWith
import net.mamoe.mirai.console.data.SerializerAwareValue import net.mamoe.mirai.console.data.SerializerAwareValue
import net.mamoe.mirai.console.data.valueFromKType import net.mamoe.mirai.console.data.valueFromKType
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType

View File

@ -52,7 +52,7 @@ internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector {
val candidatesList = candidates.toList() val candidatesList = candidates.toList()
for ((index, candidate) in candidatesList.withIndex()) { 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()}" } 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.contracts.contract
import kotlin.reflect.KClass import kotlin.reflect.KClass
/**
* The [ComponentStorage] containing all components provided by Mirai Console internals and installed plugins.
*/
internal object GlobalComponentStorage : AbstractConcurrentComponentStorage() internal object GlobalComponentStorage : AbstractConcurrentComponentStorage()
internal interface ExtensionRegistry<out E : Extension> { internal interface ExtensionRegistry<out E : Extension> {
val plugin: Plugin val plugin: Plugin?
val extension: E val extension: E
operator fun component1(): Plugin { operator fun component1(): Plugin? {
return this.plugin return this.plugin
} }
@ -35,21 +38,27 @@ internal interface ExtensionRegistry<out E : Extension> {
} }
internal class LazyExtensionRegistry<out E : Extension>( internal class LazyExtensionRegistry<out E : Extension>(
override val plugin: Plugin, override val plugin: Plugin?,
initializer: () -> E, initializer: () -> E,
) : ExtensionRegistry<E> { ) : ExtensionRegistry<E> {
override val extension: E by lazy { initializer() } override val extension: E by lazy { initializer() }
} }
internal data class DataExtensionRegistry<out E : Extension>( internal data class DataExtensionRegistry<out E : Extension>(
override val plugin: Plugin, override val plugin: Plugin?,
override val extension: E, override val extension: E,
) : ExtensionRegistry<E> ) : ExtensionRegistry<E>
internal abstract class AbstractConcurrentComponentStorage : ComponentStorage { internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal fun <T : Extension> ExtensionPoint<out T>.getExtensions(): Set<ExtensionRegistry<T>> { 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 AbstractInstanceExtensionPoint<*, *>) {
this.builtinImplementations.mapTo(HashSet()) { DataExtensionRegistry(null, it) } as Set<ExtensionRegistry<T>>
} else null
return builtins?.plus(userDefined) ?: userDefined
} }
internal fun mergeWith(another: AbstractConcurrentComponentStorage) { internal fun mergeWith(another: AbstractConcurrentComponentStorage) {
@ -68,7 +77,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution @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 { contract {
callsInPlace(block) callsInPlace(block)
} }
@ -128,11 +137,11 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
internal fun <T : Extension> ExtensionPoint<out T>.throwExtensionException( internal fun <T : Extension> ExtensionPoint<out T>.throwExtensionException(
extension: T, extension: T,
plugin: Plugin, plugin: Plugin?,
throwable: Throwable, throwable: Throwable,
) { ) {
throw ExtensionException( 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 throwable
) )
} }
@ -142,7 +151,7 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution @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) withExtensions(block)
val instances: MutableMap<ExtensionPoint<*>, MutableSet<ExtensionRegistry<*>>> = ConcurrentHashMap() val instances: MutableMap<ExtensionPoint<*>, MutableSet<ExtensionRegistry<*>>> = ConcurrentHashMap()
@ -154,6 +163,15 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(DataExtensionRegistry(plugin, extensionInstance)) 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( override fun <T : Extension> contribute(
extensionPoint: ExtensionPoint<T>, extensionPoint: ExtensionPoint<T>,
plugin: Plugin, plugin: Plugin,
@ -161,4 +179,13 @@ internal abstract class AbstractConcurrentComponentStorage : ComponentStorage {
) { ) {
instances.getOrPut(extensionPoint, ::CopyOnWriteArraySet).add(LazyExtensionRegistry(plugin, lazyInstance)) 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 { final override val parentPermission: Permission by lazy {
PermissionService.INSTANCE.register( PermissionService.INSTANCE.register(
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION), PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*"),
"The base permission" "The base permission"
) )
} }

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.console.internal.data.mkdir
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
@ -60,18 +61,17 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
override val pluginLoaders: List<PluginLoader<*, *>> override val pluginLoaders: List<PluginLoader<*, *>>
get() = _pluginLoaders.toList() get() = _pluginLoaders.toList()
override val Plugin.description: PluginDescription override fun getPluginDescription(plugin: Plugin): PluginDescription = if (plugin is JvmPlugin) {
get() = if (this is JvmPlugin) { plugin.safeLoader.getPluginDescription(plugin)
this.safeLoader.getPluginDescription(this) } else resolvedPlugins.firstOrNull { it == plugin }
} else resolvedPlugins.firstOrNull { it == this } ?.loader?.cast<PluginLoader<Plugin, PluginDescription>>()
?.loader?.cast<PluginLoader<Plugin, PluginDescription>>() ?.getPluginDescription(plugin)
?.getPluginDescription(this) ?: error("Plugin is unloaded")
?: error("Plugin is unloaded")
init { init {
MiraiConsole.coroutineContext[Job]!!.invokeOnCompletion { MiraiConsole.coroutineContext[Job]!!.invokeOnCompletion {
plugins.forEach { it.disable() } plugins.forEach { disablePlugin(it) }
} }
} }
@ -98,10 +98,10 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
this.enable(plugin as P) this.enable(plugin as P)
}.fold( }.fold(
onSuccess = { onSuccess = {
logger.info { "Successfully enabled plugin ${plugin.description.name}" } logger.info { "Successfully enabled plugin ${getPluginDescription(plugin).name}" }
}, },
onFailure = { onFailure = {
logger.info { "Cannot enable plugin ${plugin.description.name}" } logger.info { "Cannot enable plugin ${getPluginDescription(plugin).name}" }
throw it throw it
} }
) )
@ -148,7 +148,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
var count = 0 var count = 0
GlobalComponentStorage.run { GlobalComponentStorage.run {
PluginLoaderProvider.useExtensions { ext, plugin -> 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) _pluginLoaders.add(ext.instance)
count++ count++
} }
@ -166,7 +166,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
} }
internal fun enableAllLoadedPlugins() { internal fun enableAllLoadedPlugins() {
resolvedPlugins.forEach { it.enable() } resolvedPlugins.forEach { enablePlugin(it) }
} }
@kotlin.jvm.Throws(PluginLoadException::class) @kotlin.jvm.Throws(PluginLoadException::class)
@ -180,7 +180,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
private fun List<PluginLoader<*, *>>.listAndSortAllPlugins(): List<PluginDescriptionWithLoader> { private fun List<PluginLoader<*, *>>.listAndSortAllPlugins(): List<PluginDescriptionWithLoader> {
return flatMap { loader -> return flatMap { loader ->
loader.listPlugins().map { plugin -> plugin.description.wrapWith(loader, plugin) } loader.listPlugins().map { plugin -> getPluginDescription(plugin).wrapWith(loader, plugin) }
}.sortByDependencies() }.sortByDependencies()
} }

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("unused")
package net.mamoe.mirai.console.permission package net.mamoe.mirai.console.permission
import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.command.BuiltInCommands
@ -65,10 +67,10 @@ public interface Permission {
* @see RootPermission 推荐 Kotlin 用户使用. * @see RootPermission 推荐 Kotlin 用户使用.
*/ */
@JvmStatic @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 @get:JvmStatic
public val Permission.parentsWithSelf: Sequence<Permission> public val Permission.parentsWithSelf: Sequence<Permission>
@ -82,5 +84,5 @@ public interface Permission {
* 根权限. 是所有权限的父权限. 权限 ID "*:*" * 根权限. 是所有权限的父权限. 权限 ID "*:*"
*/ */
@get:JvmSynthetic @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 get() = PermissionService.INSTANCE.rootPermission

View File

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

View File

@ -17,8 +17,8 @@ import net.mamoe.mirai.console.extensions.PermissionServiceProvider
import net.mamoe.mirai.console.internal.permission.checkType import net.mamoe.mirai.console.internal.permission.checkType
import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.description import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description
import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -27,7 +27,7 @@ import kotlin.reflect.KClass
* ### 可扩展 * ### 可扩展
* 权限服务可由插件扩展并覆盖默认实现. * 权限服务可由插件扩展并覆盖默认实现.
* *
* [PermissionServiceProvider] * @see PermissionServiceProvider 相应扩展
*/ */
@PermissionImplementation @PermissionImplementation
public interface PermissionService<P : Permission> { public interface PermissionService<P : Permission> {
@ -50,11 +50,15 @@ public interface PermissionService<P : Permission> {
/** /**
* 获取所有已注册的指令列表. 应保证线程安全. * 获取所有已注册的指令列表. 应保证线程安全.
*
* 备注: Java 实现者使用 `CollectionsKt.asSequence(Collection)` 构造 [Sequence]
*/ */
public fun getRegisteredPermissions(): Sequence<P> public fun getRegisteredPermissions(): Sequence<P>
/** /**
* 获取 [PermitteeId] 和其父标识的所有被授予的所有直接和间接的权限列表 * 获取 [PermitteeId] 和其父标识的所有被授予的所有直接和间接的权限列表
*
* 备注: Java 实现者使用 `CollectionsKt.asSequence(Collection)` 构造 [Sequence]
*/ */
public fun getPermittedPermissions(permitteeId: PermitteeId): Sequence<P> public fun getPermittedPermissions(permitteeId: PermitteeId): Sequence<P>
@ -83,7 +87,12 @@ public interface PermissionService<P : Permission> {
* *
* @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出. * @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出.
* *
* @param description 描述. 将会展示给用户.
*
* @return 申请到的 [Permission] 实例 * @return 申请到的 [Permission] 实例
*
* @see get 获取一个已注册的权限
* @see getOrFail 获取一个已注册的权限
*/ */
@Throws(PermissionRegistryConflictException::class) @Throws(PermissionRegistryConflictException::class)
public fun register( public fun register(
@ -93,11 +102,14 @@ public interface PermissionService<P : Permission> {
): P ): P
/** 为 [Plugin] 分配一个 [PermissionId] */ /** 为 [Plugin] 分配一个 [PermissionId] */
@ConsoleExperimentalApi
public fun allocatePermissionIdForPlugin( public fun allocatePermissionIdForPlugin(
plugin: Plugin, plugin: Plugin,
@ResolveContext(COMMAND_NAME) permissionName: String, @ResolveContext(COMMAND_NAME) permissionName: String,
reason: PluginPermissionIdRequestType ): PermissionId = PermissionId(
): PermissionId = allocatePermissionIdForPluginDefaultImplement(plugin, permissionName, reason) plugin.description.id.toLowerCase(),
permissionName.toLowerCase()
)
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -126,102 +138,184 @@ public interface PermissionService<P : Permission> {
@Throws(UnsupportedOperationException::class) @Throws(UnsupportedOperationException::class)
public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean) public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean)
/** [Plugin] 尝试分配的 [PermissionId] 来源 */
public enum class PluginPermissionIdRequestType {
/** For [Plugin.parentPermission] */
ROOT_PERMISSION,
/** For [Plugin.permissionId] */
PERMISSION_ID
}
public companion object { public companion object {
internal var instanceField: PermissionService<*>? = null /**
* [PermissionService] 实例
*
* @see PermissionServiceProvider.selectedInstance
*/
@get:JvmName("getInstance") @get:JvmName("getInstance")
@JvmStatic @JvmStatic
public val INSTANCE: PermissionService<out Permission> public val INSTANCE: PermissionService<out Permission>
get() = instanceField ?: error("PermissionService is not yet initialized therefore cannot be used.") get() = PermissionServiceProvider.selectedInstance
/** /**
* 获取一个权限, 失败时抛出 [NoSuchElementException] * 获取一个权限, 失败时抛出 [NoSuchElementException]
*
* @see register 申请并注册一个权限
*/ */
@JvmStatic
@Throws(NoSuchElementException::class) @Throws(NoSuchElementException::class)
public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P = public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P =
get(id) ?: throw NoSuchElementException("Permission not found: $id") get(id) ?: throw NoSuchElementException("Permission not found: $id")
internal fun PermissionService<*>.allocatePermissionIdForPluginDefaultImplement( /**
plugin: Plugin, * @see findCorrespondingPermission
@ResolveContext(COMMAND_NAME) permissionName: String, */
reason: PluginPermissionIdRequestType @JvmStatic
) = PermissionId( public val PermissionId.correspondingPermission: Permission?
plugin.description.id.toLowerCase(), get() = findCorrespondingPermission()
permissionName.toLowerCase()
)
/**
* @see get
*/
@JvmStatic
public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this] public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this]
/**
* @see getOrFail
* @throws NoSuchElementException
*/
@Throws(NoSuchElementException::class)
@JvmStatic
public fun PermissionId.findCorrespondingPermissionOrFail(): Permission = INSTANCE.getOrFail(this) public fun PermissionId.findCorrespondingPermissionOrFail(): Permission = INSTANCE.getOrFail(this)
public fun PermitteeId.grantPermission(permission: Permission) { /**
* @see PermissionService.permit
*/
@JvmStatic
@JvmName("permit0") // clash, not JvmSynthetic to allow possible calls from Java.
public fun PermitteeId.permit(permission: Permission) {
INSTANCE.checkType(permission::class).permit(this, permission) INSTANCE.checkType(permission::class).permit(this, permission)
} }
public fun PermitteeId.grantPermission(permissionId: PermissionId) { /**
grantPermission(permissionId.findCorrespondingPermissionOrFail()) * @see PermissionService.permit
* @throws NoSuchElementException
*/
@JvmStatic
@Throws(NoSuchElementException::class)
public fun PermitteeId.permit(permissionId: PermissionId) {
permit(permissionId.findCorrespondingPermissionOrFail())
} }
public fun PermitteeId.denyPermission(permission: Permission, recursive: Boolean) { /**
* @see PermissionService.cancel
*/
@JvmSynthetic
@JvmStatic
@JvmName("cancel0") // clash, not JvmSynthetic to allow possible calls from Java.
public fun PermitteeId.cancel(permission: Permission, recursive: Boolean) {
INSTANCE.checkType(permission::class).cancel(this, permission, recursive) INSTANCE.checkType(permission::class).cancel(this, permission, recursive)
} }
public fun PermitteeId.denyPermission(permissionId: PermissionId, recursive: Boolean) { /**
denyPermission(permissionId.findCorrespondingPermissionOrFail(), recursive) * @see PermissionService.cancel
* @throws NoSuchElementException
*/
@JvmStatic
@Throws(NoSuchElementException::class)
public fun PermitteeId.cancel(permissionId: PermissionId, recursive: Boolean) {
cancel(permissionId.findCorrespondingPermissionOrFail(), recursive)
} }
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun Permittee.hasPermission(permission: Permission): Boolean = public fun Permittee.hasPermission(permission: Permission): Boolean =
permission.testPermission(this@hasPermission) permission.testPermission(this@hasPermission)
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun PermitteeId.hasPermission(permission: Permission): Boolean = public fun PermitteeId.hasPermission(permission: Permission): Boolean =
permission.testPermission(this@hasPermission) permission.testPermission(this@hasPermission)
/**
* @see PermissionService.testPermission
* @throws NoSuchElementException
*/
@JvmStatic
@Throws(NoSuchElementException::class)
public fun PermitteeId.hasPermission(permissionId: PermissionId): Boolean { public fun PermitteeId.hasPermission(permissionId: PermissionId): Boolean {
val instance = permissionId.findCorrespondingPermissionOrFail() val instance = permissionId.findCorrespondingPermissionOrFail()
return INSTANCE.checkType(instance::class).testPermission(this@hasPermission, instance) return INSTANCE.checkType(instance::class).testPermission(this@hasPermission, instance)
} }
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun Permittee.hasPermission(permissionId: PermissionId): Boolean = public fun Permittee.hasPermission(permissionId: PermissionId): Boolean =
permissionId.testPermission(this@hasPermission) permissionId.testPermission(this@hasPermission)
/**
* @see PermissionService.getPermittedPermissions
*/
@JvmStatic
public fun Permittee.getPermittedPermissions(): Sequence<Permission> = public fun Permittee.getPermittedPermissions(): Sequence<Permission> =
INSTANCE.getPermittedPermissions(this@getPermittedPermissions.permitteeId) INSTANCE.getPermittedPermissions(this@getPermittedPermissions.permitteeId)
public fun Permittee.grantPermission(vararg permissions: Permission) {
/**
* @see PermissionService.permit
*/
@JvmStatic
public fun Permittee.permit(vararg permissions: Permission) {
for (permission in permissions) { for (permission in permissions) {
INSTANCE.checkType(permission::class).permit(this.permitteeId, permission) INSTANCE.checkType(permission::class).permit(this.permitteeId, permission)
} }
} }
public fun Permittee.denyPermission(vararg permissions: Permission, recursive: Boolean) { /**
* @see PermissionService.cancel
*/
@JvmStatic
public fun Permittee.cancel(vararg permissions: Permission, recursive: Boolean) {
for (permission in permissions) { for (permission in permissions) {
INSTANCE.checkType(permission::class).cancel(this.permitteeId, permission, recursive) INSTANCE.checkType(permission::class).cancel(this.permitteeId, permission, recursive)
} }
} }
/**
* @see PermissionService.getPermittedPermissions
*/
@JvmSynthetic
@JvmStatic
@JvmName("getPermittedPermissions0") // clash, not JvmSynthetic to allow possible calls from Java.
public fun PermitteeId.getPermittedPermissions(): Sequence<Permission> = public fun PermitteeId.getPermittedPermissions(): Sequence<Permission> =
INSTANCE.getPermittedPermissions(this@getPermittedPermissions) INSTANCE.getPermittedPermissions(this@getPermittedPermissions)
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun Permission.testPermission(permittee: Permittee): Boolean = public fun Permission.testPermission(permittee: Permittee): Boolean =
INSTANCE.checkType(this::class).testPermission(permittee.permitteeId, this@testPermission) INSTANCE.checkType(this::class).testPermission(permittee.permitteeId, this@testPermission)
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun Permission.testPermission(permitteeId: PermitteeId): Boolean = public fun Permission.testPermission(permitteeId: PermitteeId): Boolean =
INSTANCE.checkType(this::class).testPermission(permitteeId, this@testPermission) INSTANCE.checkType(this::class).testPermission(permitteeId, this@testPermission)
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun PermissionId.testPermission(permittee: Permittee): Boolean { public fun PermissionId.testPermission(permittee: Permittee): Boolean {
val p = INSTANCE[this] ?: return false val p = INSTANCE[this] ?: return false
return p.testPermission(permittee) return p.testPermission(permittee)
} }
/**
* @see PermissionService.testPermission
*/
@JvmStatic
public fun PermissionId.testPermission(permissible: PermitteeId): Boolean { public fun PermissionId.testPermission(permissible: PermitteeId): Boolean {
val p = INSTANCE[this] ?: return false val p = INSTANCE[this] ?: return false
return p.testPermission(permissible) return p.testPermission(permissible)

View File

@ -12,23 +12,22 @@
package net.mamoe.mirai.console.plugin package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.getPluginDescription
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.SemVersion import net.mamoe.mirai.console.util.SemVersion
import kotlin.DeprecationLevel.ERROR
/** /**
* 表示一个 mirai-console 插件. * 表示一个 mirai-console 插件.
* *
* @see PluginManager.enable 启用一个插件 * @see PluginManager.enablePlugin 启用一个插件
* @see PluginManager.disable 禁用一个插件 * @see PluginManager.disablePlugin 禁用一个插件
* @see PluginManager.description 获取一个插件的 [描述][PluginDescription] * @see PluginManager.description 获取一个插件的 [描述][PluginDescription]
* *
* @see PluginDescription 插件描述 需由 [PluginLoader] 帮助提供[PluginLoader.description] * @see PluginDescription 插件描述 需由 [PluginLoader] 帮助提供[PluginLoader.getPluginDescription]
* @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件 * @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件
* @see PluginFileExtensions 支持文件系统存储的扩展 * @see PluginFileExtensions 支持文件系统存储的扩展
* *
@ -38,8 +37,8 @@ public interface Plugin : CommandOwner {
/** /**
* 判断此插件是否已启用 * 判断此插件是否已启用
* *
* @see PluginManager.enable 启用一个插件 * @see PluginManager.enablePlugin 启用一个插件
* @see PluginManager.disable 禁用一个插件 * @see PluginManager.disablePlugin 禁用一个插件
*/ */
public val isEnabled: Boolean public val isEnabled: Boolean
@ -49,32 +48,42 @@ public interface Plugin : CommandOwner {
public val loader: PluginLoader<*, *> public val loader: PluginLoader<*, *>
} }
/** @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
* 获取 [PluginDescription] @kotlin.internal.LowPriorityInOverloadResolution
*/ @Deprecated(
public inline val Plugin.description: PluginDescription get() = this.safeLoader.getPluginDescription(this) "Moved to companion for a better Java API. ",
ReplaceWith("this.description", "net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description"),
level = ERROR
)
public inline val Plugin.description: PluginDescription
get() = getPluginDescription(this) // resolved to net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.getDescription
/** /**
* 获取 [PluginDescription.name`] * 获取 [PluginDescription.name]
*/ */
public inline val Plugin.name: String get() = this.description.name public inline val Plugin.name: String get() = getPluginDescription(this).name
/**
* 获取 [PluginDescription.id]
*/
public inline val Plugin.id: String get() = getPluginDescription(this).id
/** /**
* 获取 [PluginDescription.version] * 获取 [PluginDescription.version]
*/ */
public inline val Plugin.version: SemVersion get() = this.description.version public inline val Plugin.version: SemVersion get() = getPluginDescription(this).version
/** /**
* 获取 [PluginDescription.info] * 获取 [PluginDescription.info]
*/ */
public inline val Plugin.info: String get() = this.description.info public inline val Plugin.info: String get() = getPluginDescription(this).info
/** /**
* 获取 [PluginDescription.author] * 获取 [PluginDescription.author]
*/ */
public inline val Plugin.author: String get() = this.description.author public inline val Plugin.author: String get() = getPluginDescription(this).author
/** /**
* 获取 [PluginDescription.dependencies] * 获取 [PluginDescription.dependencies]
*/ */
public inline val Plugin.dependencies: Set<PluginDependency> get() = this.description.dependencies public inline val Plugin.dependencies: Set<PluginDependency> get() = getPluginDescription(this).dependencies

View File

@ -105,45 +105,52 @@ public interface PluginManager {
/** /**
* 获取插件的 [描述][PluginDescription], 通过 [PluginLoader.getPluginDescription] * 获取插件的 [描述][PluginDescription], 通过 [PluginLoader.getPluginDescription]
*/ */
public val Plugin.description: PluginDescription public fun getPluginDescription(plugin: Plugin): PluginDescription
/** /**
* 禁用这个插件 * 禁用这个插件
* *
* @see PluginLoader.disable * @see PluginLoader.disable
*/ */
public fun Plugin.disable(): Unit = safeLoader.disable(this) public fun disablePlugin(plugin: Plugin): Unit = plugin.safeLoader.disable(plugin)
/** /**
* 加载这个插件 * 加载这个插件
* *
* @see PluginLoader.load * @see PluginLoader.load
*/ */
public fun Plugin.load(): Unit = safeLoader.load(this) public fun loadPlugin(plugin: Plugin): Unit = plugin.safeLoader.load(plugin)
/** /**
* 启用这个插件 * 启用这个插件
* *
* @see PluginLoader.enable * @see PluginLoader.enable
*/ */
public fun Plugin.enable(): Unit = safeLoader.enable(this) public fun enablePlugin(plugin: Plugin): Unit = plugin.safeLoader.enable(plugin)
/**
* 经过泛型类型转换的 [Plugin.loader]
*/
@get:JvmSynthetic
@Suppress("UNCHECKED_CAST")
public val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
get() = this.loader as PluginLoader<P, PluginDescription>
// endregion // endregion
public companion object INSTANCE : PluginManager by PluginManagerImpl { public companion object INSTANCE : PluginManager by PluginManagerImpl {
// due to Kotlin's bug /**
public override val Plugin.description: PluginDescription get() = PluginManagerImpl.run { description } * 经过泛型类型转换的 [Plugin.loader]
public override fun Plugin.disable(): Unit = PluginManagerImpl.run { disable() } */
public override fun Plugin.enable(): Unit = PluginManagerImpl.run { enable() } @get:JvmSynthetic
public override fun Plugin.load(): Unit = PluginManagerImpl.run { load() } @Suppress("UNCHECKED_CAST")
public override val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> get() = PluginManagerImpl.run { safeLoader } public inline val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
get() = this.loader as PluginLoader<P, PluginDescription>
@get:JvmSynthetic
public inline val Plugin.description: PluginDescription
get() = getPluginDescription(this)
@JvmSynthetic
public inline fun Plugin.disable(): Unit = disablePlugin(this)
@JvmSynthetic
public inline fun Plugin.enable(): Unit = enablePlugin(this)
@JvmSynthetic
public inline fun Plugin.load(): Unit = loadPlugin(this)
} }
} }

View File

@ -92,7 +92,7 @@ public interface PluginDescription {
* *
* @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本. * @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本.
*/ */
@ResolveContext(PLUGIN_VERSION) @ResolveContext(SEMANTIC_VERSION)
public val version: SemVersion public val version: SemVersion
/** /**

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 val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader
public final override fun permissionId(name: String): PermissionId = public final override fun permissionId(name: String): PermissionId =
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.PERMISSION_ID) PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name)
/** /**
* 重载 [PluginData] * 重载 [PluginData]

View File

@ -43,7 +43,7 @@ public interface JvmPluginDescription : PluginDescription {
/** /**
* @see [PluginDescription.version] * @see [PluginDescription.version]
*/ */
@ResolveContext(PLUGIN_VERSION) version: String, @ResolveContext(SEMANTIC_VERSION) version: String,
/** /**
* @see [PluginDescription.name] * @see [PluginDescription.name]
*/ */
@ -102,7 +102,7 @@ public class JvmPluginDescriptionBuilder(
) { ) {
public constructor( public constructor(
@ResolveContext(PLUGIN_ID) id: String, @ResolveContext(PLUGIN_ID) id: String,
@ResolveContext(PLUGIN_VERSION) version: String, @ResolveContext(SEMANTIC_VERSION) version: String,
) : this(id, SemVersion(version)) ) : this(id, SemVersion(version))
private var name: String = id private var name: String = id
@ -115,7 +115,7 @@ public class JvmPluginDescriptionBuilder(
apply { this.name = value.trim() } apply { this.name = value.trim() }
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun version(@ResolveContext(PLUGIN_VERSION) value: String): JvmPluginDescriptionBuilder = public fun version(@ResolveContext(SEMANTIC_VERSION) value: String): JvmPluginDescriptionBuilder =
apply { this.version = SemVersion(value) } apply { this.version = SemVersion(value) }
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever

View File

@ -14,8 +14,7 @@ package net.mamoe.mirai.console.plugin.loader
import net.mamoe.mirai.console.extensions.PluginLoaderProvider import net.mamoe.mirai.console.extensions.PluginLoaderProvider
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enablePlugin
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
@ -38,6 +37,7 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader
* 直接实现接口 [PluginLoader] [FilePluginLoader], 并注册 [PluginLoaderProvider] * 直接实现接口 [PluginLoader] [FilePluginLoader], 并注册 [PluginLoaderProvider]
* *
* @see JvmPluginLoader Jar 插件加载器 * @see JvmPluginLoader Jar 插件加载器
* @see PluginLoaderProvider 扩展
*/ */
public interface PluginLoader<P : Plugin, D : PluginDescription> { public interface PluginLoader<P : Plugin, D : PluginDescription> {
/** /**
@ -66,9 +66,9 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
public fun getPluginDescription(plugin: P): D public fun getPluginDescription(plugin: P): D
/** /**
* 主动加载一个插件 (实例), 但不 [启用][enable] . 返回加载成功的主类实例 * 主动加载一个插件 (实例), 但不 [启用][enablePlugin] . 返回加载成功的主类实例
* *
* **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] [enable], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] [enablePlugin], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException].
* *
* **实现细节**: 此函数只允许抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误. * **实现细节**: 此函数只允许抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误.
* 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件. * 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件.
@ -82,7 +82,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
/** /**
* 主动启用这个插件. * 主动启用这个插件.
* *
* **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] [enable], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException]. * **实现注意**: Console 不会把一个已经启用了的插件再次调用 [load] [enablePlugin], 但不排除意外情况. 实现本函数时应在这种情况时立即抛出异常 [IllegalStateException].
* *
* **实现细节**: 此函数可抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误. * **实现细节**: 此函数可抛出 [PluginLoadException] 作为正常失败原因, 其他任意异常都属于意外错误.
* 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件. * 当异常发生时, 插件将会直接被放弃加载, 并影响依赖它的其他插件.
@ -90,7 +90,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等).
* @throws IllegalStateException 在插件已经被加载时抛出. 这属于意料之外的情况. * @throws IllegalStateException 在插件已经被加载时抛出. 这属于意料之外的情况.
* *
* @see PluginManager.enable * @see PluginManager.enablePlugin
*/ */
@Throws(IllegalStateException::class, PluginLoadException::class) @Throws(IllegalStateException::class, PluginLoadException::class)
public fun enable(plugin: P) public fun enable(plugin: P)
@ -103,7 +103,7 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
* *
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等).
* *
* @see PluginManager.disable * @see PluginManager.disablePlugin
*/ */
@Throws(IllegalStateException::class, PluginLoadException::class) @Throws(IllegalStateException::class, PluginLoadException::class)
public fun disable(plugin: P) public fun disable(plugin: P)

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.console.util package net.mamoe.mirai.console.util
import net.mamoe.mirai.Bot 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.* 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

@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import net.mamoe.mirai.console.compiler.common.ResolveContext import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.SEMANTIC_VERSION
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.VERSION_REQUIREMENT import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.VERSION_REQUIREMENT
import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.internal.data.map
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
@ -47,10 +47,10 @@ import kotlin.LazyThreadSafetyMode.PUBLICATION
* ``` * ```
* 其中 identifier metadata 都是可选的. * 其中 identifier metadata 都是可选的.
* *
* 对于核心版本号, 此实现稍微比 semver 宽松一些, 允许 x.y 的存在. * 对于核心版本号, 此实现稍微比语义化版本规范宽松一些, 允许 x.y 的存在.
* *
* @see Requirement * @see Requirement 版本号要修
* @see SemVersion.invoke * @see SemVersion.invoke 由字符串解析
*/ */
@Serializable(with = SemVersion.SemVersionAsStringSerializer::class) @Serializable(with = SemVersion.SemVersionAsStringSerializer::class)
public data class SemVersion public data class SemVersion
@ -69,6 +69,15 @@ internal constructor(
/** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */ /** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */
public val metadata: String? = null, public val metadata: String? = null,
) : Comparable<SemVersion> { ) : Comparable<SemVersion> {
init {
require(major >= 0) { "major must >= 0" }
require(minor >= 0) { "minor must >= 0" }
if (patch != null) require(patch >= 0) { "patch must >= 0" }
if (identifier != null) require(identifier.none(Char::isWhitespace)) { "identifier must not contain whitespace" }
if (metadata != null) require(metadata.none(Char::isWhitespace)) { "metadata must not contain whitespace" }
}
/** /**
* 一条依赖规则 * 一条依赖规则
* @see [parseRangeRequirement] * @see [parseRangeRequirement]
@ -103,10 +112,10 @@ internal constructor(
* - 如果不确定版本号是否合法, 可以使用 [regex101.com](https://regex101.com/r/vkijKf/1/) 进行检查 * - 如果不确定版本号是否合法, 可以使用 [regex101.com](https://regex101.com/r/vkijKf/1/) 进行检查
* - 此实现使用的正则表达式为 `^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` * - 此实现使用的正则表达式为 `^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
*/ */
@Throws(IllegalArgumentException::class, NumberFormatException::class)
@JvmStatic @JvmStatic
@JvmName("parse") @JvmName("parse")
public operator fun invoke(@ResolveContext(PLUGIN_VERSION) version: String): SemVersion = SemVersionInternal.parse(version) @Throws(IllegalArgumentException::class, NumberFormatException::class)
public operator fun invoke(@ResolveContext(SEMANTIC_VERSION) version: String): SemVersion = SemVersionInternal.parse(version)
/** /**
* 解析一条依赖需求描述, 在无法解析的时候抛出 [IllegalArgumentException] * 解析一条依赖需求描述, 在无法解析的时候抛出 [IllegalArgumentException]
@ -138,14 +147,15 @@ internal constructor(
* - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号 * - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号
* - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()` * - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()`
*/ */
@Throws(IllegalArgumentException::class)
@JvmStatic @JvmStatic
@Throws(IllegalArgumentException::class)
public fun parseRangeRequirement(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Requirement = public fun parseRangeRequirement(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Requirement =
SemVersionInternal.parseRangeRequirement(requirement) SemVersionInternal.parseRangeRequirement(requirement)
/** @see [Requirement.test] */ /** @see [Requirement.test] */
@JvmStatic @JvmStatic
public fun Requirement.test(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(invoke(version)) @Throws(IllegalArgumentException::class, NumberFormatException::class)
public fun Requirement.test(@ResolveContext(SEMANTIC_VERSION) version: String): Boolean = test(invoke(version))
/** /**
* 当满足 [requirement] 时返回 true, 否则返回 false * 当满足 [requirement] 时返回 true, 否则返回 false
@ -157,6 +167,7 @@ internal constructor(
* 当满足 [requirement] 时返回 true, 否则返回 false * 当满足 [requirement] 时返回 true, 否则返回 false
*/ */
@JvmStatic @JvmStatic
@Throws(IllegalArgumentException::class)
public fun SemVersion.satisfies(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Boolean = parseRangeRequirement(requirement).test(this) public fun SemVersion.satisfies(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Boolean = parseRangeRequirement(requirement).test(this)
/** for Kotlin only */ /** for Kotlin only */
@ -167,7 +178,7 @@ internal constructor(
/** for Kotlin only */ /** for Kotlin only */
@JvmStatic @JvmStatic
@JvmSynthetic @JvmSynthetic
public operator fun Requirement.contains(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(version) public operator fun Requirement.contains(@ResolveContext(SEMANTIC_VERSION) version: String): Boolean = test(version)
} }
@Transient @Transient
@ -185,7 +196,10 @@ internal constructor(
} }
} }
override fun toString(): String = toString /**
* 返回类似 `1.0.0-M4+c25733b8` 的字符串.
*/
public override fun toString(): String = toString
/** /**
* [SemVersion] 转为 Kotlin data class 风格的 [String] * [SemVersion] 转为 Kotlin data class 风格的 [String]
@ -194,27 +208,60 @@ internal constructor(
return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)" return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)"
} }
override fun equals(other: Any?): Boolean { /**
if (this === other) return true * 比较 `this` [other].
if (javaClass != other?.javaClass) return false *
* @param deep `true` 时进行深度比较, 相当于 [equals]. `false` 时相当于 `compareTo(other) == 0`
other as SemVersion * @see compareTo
*/
return compareTo(other) == 0 && other.identifier == identifier && other.metadata == metadata public fun equals(other: SemVersion, deep: Boolean): Boolean {
return if (deep) {
(other.major == major
&& other.minor == minor
&& other.patch == patch
&& other.identifier == identifier
&& other.metadata == metadata)
} else {
this.compareTo(other) == 0
}
} }
override fun hashCode(): Int { /**
var result = major shl minor * 深度比较 `this` [other], 当且仅当 [major], [patch], [minor], [identifier], [metadata] 完全相同时返回 `true`.
result *= (patch ?: 1) *
* : `1.0.0-RC` != `1.0-RC`
*
* @see compareTo
*/
public override fun equals(other: Any?): Boolean {
if (other === null) return false
if (this === other) return true
if (javaClass != other.javaClass) return false
return equals(other as SemVersion, deep = true)
}
public override fun hashCode(): Int {
var result = major.hashCode()
result = 31 * result + minor.hashCode()
result = 31 * result + (patch?.hashCode() ?: 0)
result = 31 * result + (identifier?.hashCode() ?: 0) result = 31 * result + (identifier?.hashCode() ?: 0)
result = 31 * result + (metadata?.hashCode() ?: 0) result = 31 * result + (metadata?.hashCode() ?: 0)
return result return result
} }
/** /**
* Compares this object with the specified object for order. Returns zero if this object is equal * 比较 `this` [other] 的实际版本大小.
* to the specified [other] object, a negative number if it's less than [other], or a positive number *
* if it's greater than [other]. * :
* - `SemVersion("1.0.0-RC").compareTo(SemVersion("1.0-RC")) == 0` (然而对他们进行 [equals] 判断会返回 `false`)
* - `SemVersion("1.3.0") > SemVersion("1.1.0") == true` (因为 1.3.0 1.1.0 更高)
*
*
* @return `this` [other] 更高时返回一个正数.
* `this` [other] 更低时返回一个负数.
* `this` [other] 版本大小相等时返回 0.
*
* @see equals
*/ */
public override operator fun compareTo(other: SemVersion): Int { public override operator fun compareTo(other: SemVersion): Int {
return SemVersionInternal.run { compareInternal(this@SemVersion, other) } return SemVersionInternal.run { compareInternal(this@SemVersion, other) }

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 internal var cont: Continuation<Any?>? = null
@Suppress("UNCHECKED_CAST") @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 @Suppress("RemoveExplicitTypeArguments") // bug
return if (timeout != -1L) { return if (timeout != -1L) {
withTimeout<R>(timeout) { withTimeout<R>(timeout) {

View File

@ -16,14 +16,14 @@ import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.Testing import net.mamoe.mirai.console.Testing
import net.mamoe.mirai.console.Testing.withTesting import net.mamoe.mirai.console.Testing.withTesting
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.getRegisteredCommands
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 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.registerCommand
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands
import net.mamoe.mirai.console.command.description.CommandArgumentParser import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterCommand
import net.mamoe.mirai.console.command.description.buildCommandArgumentContext 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.initTestEnvironment
import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl
import net.mamoe.mirai.console.internal.command.flattenCommandComponents import net.mamoe.mirai.console.internal.command.flattenCommandComponents
@ -38,7 +38,12 @@ object TestCompositeCommand : CompositeCommand(
"testComposite", "tsC" "testComposite", "tsC"
) { ) {
@SubCommand @SubCommand
fun mute(seconds: Int) { fun mute(seconds: Int = 60) {
Testing.ok(seconds)
}
@SubCommand
fun mute(target: Long, seconds: Int) {
Testing.ok(seconds) Testing.ok(seconds)
} }
} }
@ -54,6 +59,7 @@ internal val sender by lazy { ConsoleCommandSender }
internal val owner by lazy { ConsoleCommandOwner } internal val owner by lazy { ConsoleCommandOwner }
@OptIn(ExperimentalCommandDescriptors::class)
internal class TestCommand { internal class TestCommand {
companion object { companion object {
@JvmStatic @JvmStatic
@ -72,25 +78,30 @@ internal class TestCommand {
@Test @Test
fun testRegister() { fun testRegister() {
try { try {
ConsoleCommandOwner.unregisterAllCommands() // builtins unregisterAllCommands(ConsoleCommandOwner) // builtins
unregisterCommand(TestSimpleCommand)
assertTrue(TestCompositeCommand.register()) assertTrue(TestCompositeCommand.register())
assertFalse(TestCompositeCommand.register()) assertFalse(TestCompositeCommand.register())
assertEquals(1, ConsoleCommandOwner.registeredCommands.size) assertEquals(1, getRegisteredCommands(ConsoleCommandOwner).size)
assertEquals(1, CommandManagerImpl._registeredCommands.size) assertEquals(1, CommandManagerImpl._registeredCommands.size)
assertEquals(2, CommandManagerImpl.requiredPrefixCommandMap.size) assertEquals(2,
CommandManagerImpl.requiredPrefixCommandMap.size,
CommandManagerImpl.requiredPrefixCommandMap.entries.joinToString { it.toString() })
} finally { } finally {
TestCompositeCommand.unregister() unregisterCommand(TestCompositeCommand)
} }
} }
@Test @Test
fun testSimpleExecute() = runBlocking { fun testSimpleExecute() = runBlocking {
assertEquals("test", withTesting<MessageChain> { TestSimpleCommand.withRegistration {
assertSuccess(TestSimpleCommand.execute(sender, "test")) assertEquals("test", withTesting<MessageChain> {
}.contentToString()) assertSuccess(TestSimpleCommand.execute(sender, "test"))
}.contentToString())
}
} }
@Test @Test
@ -105,24 +116,28 @@ internal class TestCommand {
@Test @Test
fun testSimpleArgsSplitting() = runBlocking { fun testSimpleArgsSplitting() = runBlocking {
assertEquals(arrayOf("test", "ttt", "tt").joinToString(), withTesting<MessageChain> { TestSimpleCommand.withRegistration {
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt"))) assertEquals(arrayOf("test", "ttt", "tt").joinToString(), withTesting<MessageChain> {
}.joinToString()) assertSuccess(TestSimpleCommand.execute(sender, PlainText("test ttt tt")))
}.joinToString())
}
} }
val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f")
@Test @Test
fun `PlainText and Image args splitting`() = runBlocking { fun `PlainText and Image args splitting`() = runBlocking {
val result = withTesting<MessageChain> { TestSimpleCommand.withRegistration {
assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain { val result = withTesting<MessageChain> {
+"test" assertSuccess(TestSimpleCommand.execute(sender, buildMessageChain {
+image +"test"
+"tt" +image
})) +"tt"
}))
}
assertEquals<Any>(arrayOf("test", image, "tt").joinToString(), result.toTypedArray().joinToString())
assertSame(image, result[1])
} }
assertEquals<Any>(arrayOf("test", image, "tt").joinToString(), result.toTypedArray().joinToString())
assertSame(image, result[1])
} }
@Test @Test
@ -134,19 +149,29 @@ internal class TestCommand {
@Test @Test
fun `executing command by string command`() = runBlocking { fun `executing command by string command`() = runBlocking {
TestCompositeCommand.register() TestCompositeCommand.withRegistration {
val result = withTesting<Int> { val result = withTesting<Int> {
assertSuccess(sender.executeCommand("/testComposite mute 1")) assertSuccess(sender.executeCommand("/testComposite mute 1"))
} }
assertEquals(1, result) 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 @Test
fun `composite command executing`() = runBlocking { fun `composite command executing`() = runBlocking {
assertEquals(1, withTesting { TestCompositeCommand.withRegistration {
assertSuccess(TestCompositeCommand.execute(sender, "mute 1")) assertEquals(1, withTesting {
}) assertSuccess(TestCompositeCommand.execute(sender, "mute 1"))
})
}
} }
@Test @Test
@ -164,19 +189,19 @@ internal class TestCommand {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
@SubCommand @SubCommand
fun mute(seconds: Int, arg2: Int) { fun mute(seconds: Int, arg2: Int = 1) {
Testing.ok(2) Testing.ok(2)
} }
} }
assertFailsWith<IllegalStateException> { registerCommand(composite)
composite.register()
} println(composite.overloads.joinToString())
/*
composite.withRegistration { composite.withRegistration {
assertEquals(1, withTesting { execute(sender, "tr", "mute 123") }) // one args, resolves to mute(Int) assertEquals(1, withTesting { assertSuccess(composite.execute(sender, "mute 123")) }) // one arg, resolves to mute(Int)
assertEquals(2, withTesting { execute(sender, "tr", "mute 123 123") }) 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`() { fun `composite sub command parsing`() {
runBlocking { runBlocking {
class MyClass( class MyClass(
val value: Int val value: Int,
) )
val composite = object : CompositeCommand( val composite = object : CompositeCommand(
ConsoleCommandOwner, ConsoleCommandOwner,
"test22", "test22",
overrideContext = buildCommandArgumentContext { overrideContext = buildCommandArgumentContext {
add(object : CommandArgumentParser<MyClass> { add(object : CommandValueArgumentParser<MyClass> {
override fun parse(raw: String, sender: CommandSender): MyClass { override fun parse(raw: String, sender: CommandSender): MyClass {
return MyClass(raw.toInt()) return MyClass(raw.toInt())
} }
override fun parse(raw: MessageContent, sender: CommandSender): MyClass { override fun parse(raw: MessageContent, sender: CommandSender): MyClass {
if (raw is PlainText) return parse(raw.content, sender)
assertSame(image, raw) assertSame(image, raw)
return MyClass(2) return MyClass(2)
} }
@ -210,12 +236,14 @@ internal class TestCommand {
} }
composite.withRegistration { 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> { assertEquals(2, withTesting<MyClass> {
execute(sender, buildMessageChain { assertSuccess(
+"mute" execute(sender, buildMessageChain {
+image +"mute"
}) +image
})
)
}.value) }.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"))
}
}
}
}
@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) { internal fun assertSuccess(result: CommandExecuteResult) {
assertTrue(result.isSuccess(), result.toString()) if (result.isFailure()) {
throw result.exception ?: AssertionError(result.toString())
}
} }

View File

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

View File

@ -22,7 +22,8 @@ internal class TestSemVersion {
internal fun testCompare() { internal fun testCompare() {
fun String.sem(): SemVersion = SemVersion.invoke(this) fun String.sem(): SemVersion = SemVersion.invoke(this)
assert("1.0".sem() < "1.0.1".sem()) assert("1.0".sem() < "1.0.1".sem())
assert("1.0.0".sem() == "1.0".sem()) assert("1.0.0".sem() != "1.0".sem())
assert("1.0.0".sem().compareTo("1.0".sem()) == 0)
assert("1.1".sem() > "1.0.0".sem()) assert("1.1".sem() > "1.0.0".sem())
assert("1.0-M4".sem() < "1.0-M5".sem()) assert("1.0-M4".sem() < "1.0-M5".sem())
assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem()) assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem())

View File

@ -8,8 +8,8 @@
*/ */
object Versions { object Versions {
const val core = "1.3.0" const val core = "1.3.2"
const val console = "1.0-RC-dev-31" const val console = "1.0-RC-dev-32"
const val consoleGraphical = "0.0.7" const val consoleGraphical = "0.0.7"
const val consoleTerminal = console const val consoleTerminal = console
@ -19,7 +19,7 @@ object Versions {
const val coroutines = "1.3.9" const val coroutines = "1.3.9"
const val collectionsImmutable = "0.3.2" const val collectionsImmutable = "0.3.2"
const val serialization = "1.0.0-RC" const val serialization = "1.0.0-RC"
const val ktor = "1.4.0" const val ktor = "1.4.1"
const val atomicFU = "0.14.4" const val atomicFU = "0.14.4"
const val androidGradle = "3.6.2" const val androidGradle = "3.6.2"

View File

@ -37,9 +37,9 @@
[`RawCommand`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt [`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 [`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 [`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 [`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/description/CommandArgumentContext.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/description/CommandArgumentContext.kt#L66 [`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 [`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`。 支持原生数据类型,`Contact` 及其子类,`Bot`。
#### 构建 [`CommandArgumentContext`] #### 构建 [`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`] 实现 ### 支持参数解析的 [`Command`] 实现
Mirai Console 内建 [`SimpleCommand`] 与 [`CompositeCommand`] 拥有 [`CommandArgumentContext`],在处理参数时会首先解析参数再传递给插件的实现。 Mirai Console 内建 [`SimpleCommand`] 与 [`CompositeCommand`] 拥有 [`CommandArgumentContext`],在处理参数时会首先解析参数再传递给插件的实现。

View File

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

View File

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

View File

@ -1,5 +1,5 @@
#Wed Mar 04 22:27:09 CST 2020 #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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -13,6 +13,7 @@ import net.mamoe.mirai.console.compiler.common.castOrNull
import net.mamoe.mirai.console.compiler.common.firstValue import net.mamoe.mirai.console.compiler.common.firstValue
import org.jetbrains.kotlin.descriptors.annotations.Annotated import org.jetbrains.kotlin.descriptors.annotations.Annotated
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.constants.ArrayValue
import org.jetbrains.kotlin.resolve.constants.EnumValue import org.jetbrains.kotlin.resolve.constants.EnumValue
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -73,11 +74,14 @@ enum class ResolveContextKind {
} }
} }
fun Annotated.isResolveContext(kind: ResolveContextKind) = this.resolveContextKind == kind val Annotated.resolveContextKinds: List<ResolveContextKind>?
val Annotated.resolveContextKind: ResolveContextKind?
get() { get() {
val ann = this.findAnnotation(RESOLVE_CONTEXT_FQ_NAME) ?: return null val ann = this.findAnnotation(RESOLVE_CONTEXT_FQ_NAME) ?: return null
val (_, enumEntryName) = ann.allValueArguments.firstValue().castOrNull<EnumValue>()?.value ?: return null // undetermined kind val kinds =
return ResolveContextKind.valueOf(enumEntryName.asString()) ann.allValueArguments.firstValue().castOrNull<ArrayValue>()?.value?.mapNotNull { it.castOrNull<EnumValue>()?.value }
?: return null // undetermined kind
return kinds.map { (_, enumEntryName) ->
ResolveContextKind.valueOf(enumEntryName.asString())
}
} }

View File

@ -10,6 +10,6 @@
package net.mamoe.mirai.console.gradle package net.mamoe.mirai.console.gradle
internal object VersionConstants { internal object VersionConstants {
const val CONSOLE_VERSION = "1.0-RC-dev-30" // value is written here automatically during build const val CONSOLE_VERSION = "1.0-RC-dev-32" // value is written here automatically during build
const val CORE_VERSION = "1.3.0" // value is written here automatically during build const val CORE_VERSION = "1.3.2" // value is written here automatically during build
} }

View File

@ -12,7 +12,7 @@ package net.mamoe.mirai.console.intellij.diagnostics
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.* import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.*
import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind
import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKind import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKinds
import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls
import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues
import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments
@ -134,12 +134,18 @@ class ContextualParametersChecker : DeclarationChecker {
context: DeclarationCheckerContext, context: DeclarationCheckerContext,
) { ) {
declaration.resolveAllCalls(context.bindingContext) declaration.resolveAllCalls(context.bindingContext)
.asSequence()
.flatMap { call -> .flatMap { call ->
call.valueParametersWithArguments().asSequence() call.valueParametersWithArguments().asSequence()
} }
.mapNotNull { (p, a) -> .mapNotNull { (p, a) ->
p.resolveContextKind?.let(checkersMap::get)?.let { it to a } p.resolveContextKinds
?.map(checkersMap::get)
?.mapNotNull {
if (it == null) null else it to a
}
} }
.flatMap { it.asSequence() }
.mapNotNull { (kind, argument) -> .mapNotNull { (kind, argument) ->
argument.resolveStringConstantValues()?.let { const -> argument.resolveStringConstantValues()?.let { const ->
Triple(kind, argument, const) Triple(kind, argument, const)

View File

@ -31,12 +31,12 @@ class PluginDataValuesChecker : DeclarationChecker {
declaration.resolveAllCallsWithElement(bindingContext) declaration.resolveAllCallsWithElement(bindingContext)
.filter { (call) -> call.isCalling(PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME) } .filter { (call) -> call.isCalling(PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME) }
.filter { (call) -> .filter { (call) ->
call.resultingDescriptor.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR call.resultingDescriptor.resolveContextKinds?.contains(ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) == true
}.flatMap { (call, element) -> }.flatMap { (call, element) ->
call.typeArguments.entries.associateWith { element }.asSequence() call.typeArguments.entries.associateWith { element }.asSequence()
}.filter { (e, _) -> }.filter { (e, _) ->
val (p, t) = e val (p, t) = e
(p.isReified || p.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) (p.isReified || p.resolveContextKinds?.contains(ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) == true)
&& t is SimpleType && t is SimpleType
}.forEach { (e, callExpr) -> }.forEach { (e, callExpr) ->
val (_, type) = e val (_, type) = e

View File

@ -15,7 +15,6 @@ import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
fun DeclarationCheckerContext.report(diagnostic: Diagnostic) { fun DeclarationCheckerContext.report(diagnostic: Diagnostic) {
return this.trace.report(diagnostic) return this.trace.report(diagnostic)
@ -25,7 +24,6 @@ val DeclarationCheckerContext.bindingContext get() = this.trace.bindingContext
fun KtElement?.getResolvedCallOrResolveToCall( fun KtElement?.getResolvedCallOrResolveToCall(
context: DeclarationCheckerContext, context: DeclarationCheckerContext,
bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL,
): ResolvedCall<out CallableDescriptor>? { ): ResolvedCall<out CallableDescriptor>? {
return this.getResolvedCallOrResolveToCall(context.bindingContext, bodyResolveMode) return this.getResolvedCallOrResolveToCall(context.bindingContext)
} }

View File

@ -32,7 +32,6 @@ import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.constants.ArrayValue import org.jetbrains.kotlin.resolve.constants.ArrayValue
import org.jetbrains.kotlin.resolve.constants.ConstantValue import org.jetbrains.kotlin.resolve.constants.ConstantValue
import org.jetbrains.kotlin.resolve.constants.StringValue import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
@ -125,9 +124,8 @@ inline fun <reified E> PsiElement.findChild(): E? = this.children.find { it is E
fun KtElement?.getResolvedCallOrResolveToCall( fun KtElement?.getResolvedCallOrResolveToCall(
context: BindingContext, context: BindingContext,
bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL,
): ResolvedCall<out CallableDescriptor>? { ): ResolvedCall<out CallableDescriptor>? {
return this?.getCall(context)?.getResolvedCall(context)// ?: this?.resolveToCall(bodyResolveMode) return this?.getCall(context)?.getResolvedCall(context)
} }
val ResolvedCall<out CallableDescriptor>.valueParameters: List<ValueParameterDescriptor> get() = this.resultingDescriptor.valueParameters val ResolvedCall<out CallableDescriptor>.valueParameters: List<ValueParameterDescriptor> get() = this.resultingDescriptor.valueParameters