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 kotlin.annotation.AnnotationTarget.*
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
/**
@ -176,7 +177,7 @@ public interface MiraiConsoleImplementation : CoroutineScope {
/**
* 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例
*
* 必须在 [start] 之后才能使用.
* 必须在 [start] 之后才能使用, 否则抛出 [UninitializedPropertyAccessException]
*/
@JvmStatic
@ConsoleFrontEndImplementation
@ -189,7 +190,22 @@ public interface MiraiConsoleImplementation : CoroutineScope {
public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock {
if (::instance.isInitialized) error("Mirai Console is already initialized.")
this@Companion.instance = this
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.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.descriptor.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.allRegisteredCommands
import net.mamoe.mirai.console.internal.util.runIgnoreException
import net.mamoe.mirai.console.permission.Permission
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.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.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInternalApi
@ -150,11 +154,11 @@ public object BuiltInCommands {
ConsoleCommandOwner, "permission", "权限", "perm",
description = "管理权限",
overrideContext = buildCommandArgumentContext {
PermitteeId::class with PermitteeIdArgumentParser
Permission::class with PermissionIdArgumentParser.map { id ->
PermitteeId::class with PermitteeIdValueArgumentParser
Permission::class with PermissionIdValueArgumentParser.map { id ->
kotlin.runCatching {
id.findCorrespondingPermissionOrFail()
}.getOrElse { illegalArgument("指令不存在: $id", it) }
}.getOrElse { throw CommandArgumentParserException("指令不存在: $id", it) }
}
},
), BuiltInCommandInternal {
@ -166,7 +170,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.grantPermission(permission)
target.permit(permission)
sendMessage("OK")
}
@ -176,7 +180,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.denyPermission(permission, false)
target.cancel(permission, false)
sendMessage("OK")
}
@ -186,7 +190,7 @@ public object BuiltInCommands {
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.denyPermission(permission, true)
target.cancel(permission, true)
sendMessage("OK")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,21 +5,23 @@
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.command.description
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.descriptor.CommandArgumentParserException
/**
* 解析参数时遇到的 _正常_ 错误. 如参数不符合规范.
* 处理参数时遇到的 _正常_ 错误. 如参数不符合规范, 参数值越界.
*
* [message] 将会发送给指令调用方.
*
* @see CommandArgumentParser
* @see CommandArgumentParser.illegalArgument
* @see CommandArgumentParserException
*/
public class CommandArgumentParserException : RuntimeException {
public open class IllegalCommandArgumentException : IllegalArgumentException {
public constructor() : super()
public constructor(message: String?) : super(message)
public constructor(message: String?, cause: Throwable?) : super(message, cause)

View File

@ -11,14 +11,16 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.java.JRawCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.buildMessageChain
/**
* 无参数解析, 接收原生参数的指令.
@ -48,10 +50,23 @@ public abstract class RawCommand(
/** 指令父权限 */
parentPermission: Permission = owner.parentPermission,
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@OptIn(ExperimentalCommandDescriptors::class)
public override val prefixOptional: Boolean = false,
) : Command {
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 查看更多信息
*/
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
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.descriptor.*
import net.mamoe.mirai.console.command.java.JSimpleCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand
import net.mamoe.mirai.console.internal.command.CommandReflector
import net.mamoe.mirai.console.internal.command.IllegalCommandDeclarationException
import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
/**
* 简单的, 支持参数自动解析的指令.
*
* 要查看指令解析流程, 参考 [CommandManager.executeCommand]
* 要查看参数解析方式, 参考 [CommandArgumentParser]
* 要查看参数解析方式, 参考 [CommandValueArgumentParser]
*
* Kotlin 实现:
* ```
@ -58,39 +60,42 @@ public abstract class SimpleCommand(
parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
) : Command, AbstractCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware {
private val reflector by lazy { CommandReflector(this, SimpleCommandSubCommandAnnotationResolver) }
@ExperimentalCommandDescriptors
public final override val overloads: List<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]. 也可以被覆盖.
*/
public override val usage: String get() = super.usage
public override val usage: String by lazy {
@OptIn(ExperimentalCommandDescriptors::class)
reflector.generateUsage(overloads)
}
/**
* 标注指令处理器
*/
@Target(FUNCTION)
protected annotation class Handler
/** 参数名, 将参与构成 [usage] */
@ConsoleExperimentalApi("Classname might change")
@Target(VALUE_PARAMETER)
protected annotation class Name(val value: String)
/**
* 指令参数环境. 默认为 [CommandArgumentContext.Builtins] `+` `overrideContext`
*/
public override val context: CommandArgumentContext = CommandArgumentContext.Builtins + overrideContext
public final override suspend fun CommandSender.onCommand(args: MessageChain) {
subCommands.single().parseAndExecute(this, args, false)
}
internal override fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) {
super.checkSubCommand(subCommands)
check(subCommands.size == 1) { "There can only be exactly one function annotated with Handler at this moment as overloading is not yet supported." }
}
@Deprecated("prohibited", level = DeprecationLevel.HIDDEN)
internal override suspend fun CommandSender.onDefault(rawArgs: MessageChain) {
sendMessage(usage)
}
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
get() = SimpleCommandSubCommandAnnotationResolver
}

View File

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

View File

@ -7,7 +7,9 @@
* 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.console.command.*
@ -25,47 +27,47 @@ import net.mamoe.mirai.message.data.*
/**
* 使用 [String.toInt] 解析
*/
public object IntArgumentParser : InternalCommandArgumentParserExtensions<Int> {
public object IntValueArgumentParser : InternalCommandValueArgumentParserExtensions<Int>() {
public override fun parse(raw: String, sender: CommandSender): Int =
raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toLong] 解析
*/
public object LongArgumentParser : InternalCommandArgumentParserExtensions<Long> {
public object LongValueArgumentParser : InternalCommandValueArgumentParserExtensions<Long>() {
public override fun parse(raw: String, sender: CommandSender): Long =
raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toShort] 解析
*/
public object ShortArgumentParser : InternalCommandArgumentParserExtensions<Short> {
public object ShortValueArgumentParser : InternalCommandValueArgumentParserExtensions<Short>() {
public override fun parse(raw: String, sender: CommandSender): Short =
raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toByte] 解析
*/
public object ByteArgumentParser : InternalCommandArgumentParserExtensions<Byte> {
public object ByteValueArgumentParser : InternalCommandValueArgumentParserExtensions<Byte>() {
public override fun parse(raw: String, sender: CommandSender): Byte =
raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toDouble] 解析
*/
public object DoubleArgumentParser : InternalCommandArgumentParserExtensions<Double> {
public object DoubleValueArgumentParser : InternalCommandValueArgumentParserExtensions<Double>() {
public override fun parse(raw: String, sender: CommandSender): Double =
raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
/**
* 使用 [String.toInt] 解析
* 使用 [String.toFloat] 解析
*/
public object FloatArgumentParser : InternalCommandArgumentParserExtensions<Float> {
public object FloatValueArgumentParser : InternalCommandValueArgumentParserExtensions<Float>() {
public override fun parse(raw: String, sender: CommandSender): Float =
raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
@ -73,14 +75,14 @@ public object FloatArgumentParser : InternalCommandArgumentParserExtensions<Floa
/**
* 直接返回 [String], 或取用 [SingleMessage.contentToString]
*/
public object StringArgumentParser : InternalCommandArgumentParserExtensions<String> {
public object StringValueArgumentParser : InternalCommandValueArgumentParserExtensions<String>() {
public override fun parse(raw: String, sender: CommandSender): String = raw
}
/**
* 解析 [String] 通过 [Image].
*/
public object ImageArgumentParser : InternalCommandArgumentParserExtensions<Image> {
public object ImageValueArgumentParser : InternalCommandValueArgumentParserExtensions<Image>() {
public override fun parse(raw: String, sender: CommandSender): Image {
return kotlin.runCatching {
Image(raw)
@ -95,7 +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 {
return PlainText(raw)
}
@ -109,7 +111,7 @@ public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions<
/**
* 当字符串内容为(不区分大小写) "true", "yes", "enabled"
*/
public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> {
public object BooleanValueArgumentParser : InternalCommandValueArgumentParserExtensions<Boolean>() {
public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true)
@ -121,7 +123,7 @@ public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Bo
/**
* 根据 [Bot.id] 解析一个登录后的 [Bot]
*/
public object ExistingBotArgumentParser : InternalCommandArgumentParserExtensions<Bot> {
public object ExistingBotValueArgumentParser : InternalCommandValueArgumentParserExtensions<Bot>() {
public override fun parse(raw: String, sender: CommandSender): Bot =
if (raw == "~") sender.inferBotOrFail()
else raw.findBotOrFail()
@ -136,7 +138,7 @@ public object ExistingBotArgumentParser : InternalCommandArgumentParserExtension
/**
* 解析任意一个存在的好友.
*/
public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtensions<Friend> {
public object ExistingFriendValueArgumentParser : InternalCommandValueArgumentParserExtensions<Friend>() {
private val syntax = """
- `botId.friendId`
- `botId.friendNick` (模糊搜索, 寻找最优匹配)
@ -175,7 +177,7 @@ public object ExistingFriendArgumentParser : InternalCommandArgumentParserExtens
/**
* 解析任意一个存在的群.
*/
public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensions<Group> {
public object ExistingGroupValueArgumentParser : InternalCommandValueArgumentParserExtensions<Group>() {
private val syntax = """
- `botId.groupId`
- `~` (指代指令调用人自己所在群. 仅群聊天环境下)
@ -202,7 +204,7 @@ public object ExistingGroupArgumentParser : InternalCommandArgumentParserExtensi
}
}
public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensions<User> {
public object ExistingUserValueArgumentParser : InternalCommandValueArgumentParserExtensions<User>() {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -215,11 +217,11 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
""".trimIndent()
override fun parse(raw: String, sender: CommandSender): User {
return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse)
return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse)
}
override fun parse(raw: MessageContent, sender: CommandSender): User {
return parseImpl(sender, raw, ExistingMemberArgumentParser::parse, ExistingFriendArgumentParser::parse)
return parseImpl(sender, raw, ExistingMemberValueArgumentParser::parse, ExistingFriendValueArgumentParser::parse)
}
private fun <T> parseImpl(
@ -246,7 +248,7 @@ public object ExistingUserArgumentParser : InternalCommandArgumentParserExtensio
}
public object ExistingContactArgumentParser : InternalCommandArgumentParserExtensions<Contact> {
public object ExistingContactValueArgumentParser : InternalCommandValueArgumentParserExtensions<Contact>() {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -259,11 +261,11 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
""".trimIndent()
override fun parse(raw: String, sender: CommandSender): Contact {
return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse)
return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse)
}
override fun parse(raw: MessageContent, sender: CommandSender): Contact {
return parseImpl(sender, raw, ExistingUserArgumentParser::parse, ExistingGroupArgumentParser::parse)
return parseImpl(sender, raw, ExistingUserValueArgumentParser::parse, ExistingGroupValueArgumentParser::parse)
}
private fun <T> parseImpl(
@ -286,7 +288,7 @@ public object ExistingContactArgumentParser : InternalCommandArgumentParserExten
/**
* 解析任意一个群成员.
*/
public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtensions<Member> {
public object ExistingMemberValueArgumentParser : InternalCommandValueArgumentParserExtensions<Member>() {
private val syntax: String = """
- `botId.groupId.memberId`
- `botId.groupId.memberCard` (模糊搜索, 寻找最优匹配)
@ -333,7 +335,7 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens
}
}
public object PermissionIdArgumentParser : CommandArgumentParser<PermissionId> {
public object PermissionIdValueArgumentParser : InternalCommandValueArgumentParserExtensions<PermissionId>() {
override fun parse(raw: String, sender: CommandSender): PermissionId {
return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse {
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 {
return if (raw == "~") sender.permitteeId
else kotlin.runCatching { AbstractPermitteeId.parseFromString(raw) }.getOrElse {
@ -351,32 +353,38 @@ public object PermitteeIdArgumentParser : CommandArgumentParser<PermitteeId> {
override fun parse(raw: MessageContent, sender: CommandSender): PermitteeId {
if (raw is At) {
return ExistingUserArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId
return ExistingUserValueArgumentParser.parse(raw, sender).asCommandSender(false).permitteeId
}
return super.parse(raw, sender)
}
}
internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArgumentParser<T> {
fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数")
/** 直接返回原始参数 [MessageContent] */
public object RawContentValueArgumentParser : CommandValueArgumentParser<MessageContent> {
override fun parse(raw: String, sender: CommandSender): MessageContent = PlainText(raw)
override fun parse(raw: MessageContent, sender: CommandSender): MessageContent = raw
}
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")
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")
fun Bot.findFriendOrFail(id: String): Friend =
protected fun Bot.findFriendOrFail(id: String): Friend =
getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到好友: $this")
fun Bot.findMemberOrFail(id: String): Friend =
protected fun Bot.findMemberOrFail(id: String): Friend =
getFriendOrNull(id.parseToLongOrFail()) ?: illegalArgument("无法找到群员: $this")
fun Group.findMemberOrFail(idOrCard: String): Member {
protected fun Group.findMemberOrFail(idOrCard: String): Member {
if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员")
idOrCard.toLongOrNull()?.let { getOrNull(it) }?.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
?: Bot.botInstancesSequence.singleOrNull()
?: illegalArgument("当前语境下无法推断目标 Bot, 因为目前有多个 Bot 在线.")
fun CommandSender.inferGroupOrFail(): Group =
protected fun CommandSender.inferGroupOrFail(): Group =
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("当前语境下无法推断目标好友")
}
internal fun Double.toDecimalPlace(n: Int): String {
return "%.${n}f".format(this)
}
internal fun Double.toDecimalPlace(n: Int): String = "%.${n}f".format(this)
internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString {
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.CommandOwner
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.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission
@ -83,6 +84,7 @@ public abstract class JCompositeCommand
protected set
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@ExperimentalCommandDescriptors
public final override var prefixOptional: Boolean = false
protected set

View File

@ -9,16 +9,15 @@
package net.mamoe.mirai.console.command.java
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
/**
* Java 用户继承
@ -70,21 +69,7 @@ public abstract class JRawCommand
protected set
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
@ExperimentalCommandDescriptors
public final override var prefixOptional: Boolean = false
protected set
/**
* 在指令被执行时调用.
*
* @param args 指令参数. 数组元素类型可能是 [SingleMessage] [String]. 且已经以 ' ' 分割.
*
* @see CommandManager.execute 查看更多信息
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("onCommand")
public abstract fun onCommand(sender: CommandSender, args: MessageChain)
public final override suspend fun CommandSender.onCommand(args: MessageChain) {
withContext(Dispatchers.IO) { onCommand(this@onCommand, args) }
}
}

View File

@ -10,10 +10,10 @@
package net.mamoe.mirai.console.command.java
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.console.command.description.CommandArgumentContext
import net.mamoe.mirai.console.command.descriptor.CommandArgumentContext
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission
@ -50,9 +50,10 @@ public abstract class JSimpleCommand(
) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) {
public override var description: String = super.description
protected set
public override var permission: Permission = super.permission
protected set
@ExperimentalCommandDescriptors
public override var prefixOptional: Boolean = super.prefixOptional
protected set
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
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.*
/**
* 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断.
*/
@ConsoleExperimentalApi
@Target(
VALUE_PARAMETER,
PROPERTY, FIELD,
FUNCTION,
TYPE, TYPE_PARAMETER
)
@Target(VALUE_PARAMETER, PROPERTY, FIELD, FUNCTION, TYPE, TYPE_PARAMETER)
@Retention(AnnotationRetention.BINARY)
public annotation class ResolveContext(
val kind: Kind,
vararg val kinds: Kind,
) {
/**
* 元素数量可能在任意时间被改动
@ -36,18 +35,57 @@ public annotation class ResolveContext(
// ConstantKind
///////////////////////////////////////////////////////////////////////////
PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION
PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION
/*
* WARNING: IF YOU CHANGE NAMES HERE,
* 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
/**
* @see Command.allNames
*/
COMMAND_NAME, // ILLEGAL_COMMAND_NAME
PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE
PERMISSION_NAME, // ILLEGAL_COMMAND_NAME
PERMISSION_ID, // ILLEGAL_COMMAND_ID
/**
* @see PermissionId.name
*/
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
}
}

View File

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

View File

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

View File

@ -7,24 +7,72 @@
* 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
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
/**
* [Extension] `companion` 实现.
* [Extension] 的伴生对象实现.
*
* @see AbstractExtensionPoint
*/
public interface ExtensionPoint<T : Extension> {
/**
* 扩展实例 [T] 的类型
*/
public val extensionType: KClass<T>
}
public open class AbstractExtensionPoint<T : Extension>(
public abstract class AbstractExtensionPoint<T : Extension>(
public override val extensionType: KClass<T>,
) : ExtensionPoint<T>
/**
* 表示一个 [SingletonExtension] [ExtensionPoint]
*/
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
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.internal.extension.AbstractConcurrentComponentStorage
import net.mamoe.mirai.console.permission.PermissionService
@ -35,7 +37,7 @@ public class PluginComponentStorage(
): Unit = contribute(extensionPoint, plugin, lazyInstance)
/**
* 注册一个扩展
* 注册一个扩展. [E] 必须拥有伴生对象为 [ExtensionPoint].
*/
public inline fun <reified E : Extension> contribute(
noinline lazyInstance: () -> E,
@ -56,6 +58,7 @@ public class PluginComponentStorage(
public fun contributeSingletonExtensionSelector(lazyInstance: () -> SingletonExtensionSelector): Unit =
contribute(SingletonExtensionSelector, plugin, lazyInstance)
@Suppress("SpellCheckingInspection") // alterer
/** 注册一个 [BotConfigurationAlterer] */
public fun contributeBotConfigurationAlterer(instance: BotConfigurationAlterer): Unit =
contribute(BotConfigurationAlterer, plugin, lazyInstance = { instance })
@ -73,16 +76,14 @@ public class PluginComponentStorage(
/** 注册一个 [PermissionServiceProvider] */
@OverloadResolutionByLambdaReturnType
public fun contributePermissionService(
lazyInstance: () -> PermissionService<*>,
): Unit = contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance))
public fun contributePermissionService(lazyInstance: () -> PermissionService<*>): Unit =
contribute(PermissionServiceProvider, plugin, LazyPermissionServiceProviderImpl(lazyInstance))
/** 注册一个 [PermissionServiceProvider] */
@JvmName("contributePermissionServiceProvider")
@OverloadResolutionByLambdaReturnType
public fun contributePermissionService(
lazyProvider: () -> PermissionServiceProvider,
): Unit = contribute(PermissionServiceProvider, plugin, lazyProvider)
public fun contributePermissionService(lazyProvider: () -> PermissionServiceProvider): Unit =
contribute(PermissionServiceProvider, plugin, lazyProvider)
/////////////////////////////////////
@ -95,5 +96,20 @@ public class PluginComponentStorage(
@JvmName("contributePluginLoaderProvider")
@OverloadResolutionByLambdaReturnType
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
*/
@Suppress("SpellCheckingInspection") // alterer
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
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.SingletonExtensionPoint
import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService
import net.mamoe.mirai.console.permission.PermissionService
/**
@ -21,8 +21,7 @@ import net.mamoe.mirai.console.permission.PermissionService
*/
public interface PermissionServiceProvider : SingletonExtension<PermissionService<*>> {
public companion object ExtensionPoint :
AbstractExtensionPoint<PermissionServiceProvider>(PermissionServiceProvider::class),
SingletonExtensionPoint<PermissionServiceProvider>
AbstractSingletonExtensionPoint<PermissionServiceProvider, PermissionService<*>>(PermissionServiceProvider::class, BuiltInPermissionService)
}
/**

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ import net.mamoe.mirai.console.internal.util.autoHexToBytes
import net.mamoe.mirai.console.logging.*
import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger
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.plugin.PluginManager
import net.mamoe.mirai.console.plugin.center.PluginCenter
@ -177,9 +177,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
phase `load PermissionService`@{
mainLogger.verbose { "Loading PermissionService..." }
PermissionService.instanceField = GlobalComponentStorage.run {
PermissionServiceProvider.findSingletonInstance(BuiltInPermissionService)
}
PermissionServiceProvider.selectedInstance // init
PermissionService.INSTANCE.let { ps ->
if (ps is BuiltInPermissionService) {
@ -188,7 +186,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
}
}
ConsoleCommandSender.grantPermission(RootPermission)
ConsoleCommandSender.permit(RootPermission)
}
phase `prepare commands`@{
@ -231,7 +229,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
}
GlobalComponentStorage.run {
PostStartupExtension.useExtensions { it() }
PostStartupExtension.useExtensions { it() } // exceptions thrown will be caught by caller of `doStart`.
}
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.command.*
import net.mamoe.mirai.console.command.Command.Companion.allNames
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.findDuplicate
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
import net.mamoe.mirai.console.command.parse.CommandCallParser.Companion.parseCommandCall
import net.mamoe.mirai.console.command.resolve.CommandCallResolver.Companion.resolve
import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.message.MessageEvent
import net.mamoe.mirai.message.data.EmptyMessageChain
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.utils.MiraiLogger
import java.util.concurrent.locks.ReentrantLock
internal object CommandManagerImpl : CommandManager, CoroutineScope by CoroutineScope(MiraiConsole.job) {
@OptIn(ExperimentalCommandDescriptors::class)
internal object CommandManagerImpl : CommandManager, CoroutineScope by MiraiConsole.childScope("CommandManagerImpl") {
private val logger: MiraiLogger by lazy {
MiraiConsole.createLogger("command")
}
@ -48,11 +55,11 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
/**
* 从原始的 command 中解析出 Command 对象
*/
internal fun matchCommand(rawCommand: String): Command? {
if (rawCommand.startsWith(commandPrefix)) {
return requiredPrefixCommandMap[rawCommand.substringAfter(commandPrefix).toLowerCase()]
override fun matchCommand(commandName: String): Command? {
if (commandName.startsWith(commandPrefix)) {
return requiredPrefixCommandMap[commandName.substringAfter(commandPrefix).toLowerCase()]
}
return optionalPrefixCommandMap[rawCommand.toLowerCase()]
return optionalPrefixCommandMap[commandName.toLowerCase()]
}
internal val commandListener: Listener<MessageEvent> by lazy {
@ -65,13 +72,17 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
) {
val sender = this.toCommandSender()
when (val result = sender.executeCommand(message)) {
when (val result = executeCommand(sender, message)) {
is CommandExecuteResult.PermissionDenied -> {
if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) {
sender.sendMessage("权限不足")
intercept()
}
}
is CommandExecuteResult.IllegalArgument -> {
result.exception.message?.let { sender.sendMessage(it) }
intercept()
}
is CommandExecuteResult.Success -> {
intercept()
}
@ -79,7 +90,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
sender.catchExecutionException(result.exception)
intercept()
}
is CommandExecuteResult.CommandNotFound -> {
is CommandExecuteResult.UnresolvedCall -> {
// noop
}
}
@ -90,102 +101,90 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
///// IMPL
override val CommandOwner.registeredCommands: List<Command> get() = _registeredCommands.filter { it.owner == this }
override fun getRegisteredCommands(owner: CommandOwner): List<Command> = _registeredCommands.filter { it.owner == owner }
override val allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy
override val commandPrefix: String get() = "/"
override fun CommandOwner.unregisterAllCommands() {
for (registeredCommand in registeredCommands) {
registeredCommand.unregister()
override fun unregisterAllCommands(owner: CommandOwner) {
for (registeredCommand in getRegisteredCommands(owner)) {
unregisterCommand(registeredCommand)
}
}
override fun Command.register(override: Boolean): Boolean {
if (this is CompositeCommand) this.subCommands // init lazy
override fun registerCommand(command: Command, override: Boolean): Boolean {
if (command is CompositeCommand) {
command.overloads // init lazy
}
kotlin.runCatching {
this.permission // init lazy
this.secondaryNames // init lazy
this.description // init lazy
this.usage // init lazy
command.permission // init lazy
command.secondaryNames // init lazy
command.description // init lazy
command.usage // init lazy
}.onFailure {
throw IllegalStateException("Failed to init command ${this@register}.", it)
throw IllegalStateException("Failed to init command ${command}.", it)
}
modifyLock.withLock {
this@CommandManagerImpl.modifyLock.withLock {
if (!override) {
if (findDuplicate() != null) return false
if (command.findDuplicate() != null) return false
}
_registeredCommands.add(this@register)
if (this.prefixOptional) {
for (name in this.allNames) {
this@CommandManagerImpl._registeredCommands.add(command)
if (command.prefixOptional) {
for (name in command.allNames) {
val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap[lowerCaseName] = this
requiredPrefixCommandMap[lowerCaseName] = this
this@CommandManagerImpl.optionalPrefixCommandMap[lowerCaseName] = command
this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
}
} else {
for (name in this.allNames) {
for (name in command.allNames) {
val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
requiredPrefixCommandMap[lowerCaseName] = this
this@CommandManagerImpl.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
this@CommandManagerImpl.requiredPrefixCommandMap[lowerCaseName] = command
}
}
return true
}
}
override fun Command.findDuplicate(): Command? =
_registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames }
override fun findDuplicateCommand(command: Command): Command? =
_registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase command.allNames }
override fun Command.unregister(): Boolean = modifyLock.withLock {
if (this.prefixOptional) {
this.allNames.forEach {
optionalPrefixCommandMap.remove(it)
override fun unregisterCommand(command: Command): Boolean = modifyLock.withLock {
if (command.prefixOptional) {
command.allNames.forEach {
optionalPrefixCommandMap.remove(it.toLowerCase())
}
}
this.allNames.forEach {
requiredPrefixCommandMap.remove(it)
command.allNames.forEach {
requiredPrefixCommandMap.remove(it.toLowerCase())
}
_registeredCommands.remove(this)
_registeredCommands.remove(command)
}
override fun Command.isRegistered(): Boolean = this in _registeredCommands
override suspend fun Command.execute(
sender: CommandSender,
arguments: Message,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
arguments.flattenCommandComponents(),
primaryName,
checkPermission
)
override fun isCommandRegistered(command: Command): Boolean = command in _registeredCommands
}
override suspend fun Command.execute(
sender: CommandSender,
arguments: String,
checkPermission: Boolean
): CommandExecuteResult {
return sender.executeCommandInternal(
this,
arguments.flattenCommandComponents(),
primaryName,
checkPermission
)
}
override suspend fun CommandSender.executeCommand(
// Don't move into CommandManager, compilation error / VerifyError
@OptIn(ExperimentalCommandDescriptors::class)
internal suspend fun executeCommandImpl(
message: Message,
checkPermission: Boolean
caller: CommandSender,
checkPermission: Boolean,
): CommandExecuteResult {
val msg = message.asMessageChain().filterIsInstance<MessageContent>()
if (msg.isEmpty()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(msg, msg[0].content.substringBefore(' '), checkPermission)
val call = message.asMessageChain().parseCommandCall(caller) ?: return CommandExecuteResult.UnresolvedCall("")
val resolved = call.resolve() ?: return CommandExecuteResult.UnresolvedCall(call.calleeName)
val command = resolved.callee
if (checkPermission && !command.permission.testPermission(caller)) {
return CommandExecuteResult.PermissionDenied(command, call.calleeName)
}
override suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean): CommandExecuteResult {
if (message.isBlank()) return CommandExecuteResult.CommandNotFound("")
return executeCommandInternal(message, message.substringBefore(' '), checkPermission)
return try {
resolved.calleeSignature.call(resolved)
CommandExecuteResult.Success(resolved.callee, call.calleeName, EmptyMessageChain)
} catch (e: Throwable) {
CommandExecuteResult.ExecutionFailed(e, resolved.callee, call.calleeName, EmptyMessageChain)
}
}

View File

@ -0,0 +1,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
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 {
val max = this.size.coerceAtMost(other.size)
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 net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SilentLogger
@ -74,7 +73,9 @@ internal open class MultiFilePluginDataStorageImpl(
public override fun store(holder: PluginDataHolder, instance: PluginData) {
getPluginDataFile(holder, instance).writeText(
kotlin.runCatching {
yaml.encodeToString(instance.updaterSerializer, Unit)
yaml.encodeToString(instance.updaterSerializer, Unit).also {
yaml.decodeAnyFromString(it) // test yaml
}
}.recoverCatching {
// Just use mainLogger for convenience.
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.ValueName
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.*
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null
@Suppress("UNCHECKED_CAST")
internal inline fun <reified T : Any> KType.toKClass(): KClass<out T> {
val clazz = requireNotNull(classifier as? KClass<T>) { "Unsupported classifier: $classifier" }
@ -54,6 +55,12 @@ internal fun KType.classifierAsKClass() = when (val t = classifier) {
else -> error("Only KClass supported as classifier, got $t")
} as KClass<Any>
@Suppress("UNCHECKED_CAST")
internal fun KType.classifierAsKClassOrNull() = when (val t = classifier) {
is KClass<*> -> t
else -> null
} as KClass<Any>?
@JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ internal abstract class JvmPluginInternal(
final override val parentPermission: Permission by lazy {
PermissionService.INSTANCE.register(
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION),
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*"),
"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.plugin.Plugin
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.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
@ -60,18 +61,17 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
override val pluginLoaders: List<PluginLoader<*, *>>
get() = _pluginLoaders.toList()
override val Plugin.description: PluginDescription
get() = if (this is JvmPlugin) {
this.safeLoader.getPluginDescription(this)
} else resolvedPlugins.firstOrNull { it == this }
override fun getPluginDescription(plugin: Plugin): PluginDescription = if (plugin is JvmPlugin) {
plugin.safeLoader.getPluginDescription(plugin)
} else resolvedPlugins.firstOrNull { it == plugin }
?.loader?.cast<PluginLoader<Plugin, PluginDescription>>()
?.getPluginDescription(this)
?.getPluginDescription(plugin)
?: error("Plugin is unloaded")
init {
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)
}.fold(
onSuccess = {
logger.info { "Successfully enabled plugin ${plugin.description.name}" }
logger.info { "Successfully enabled plugin ${getPluginDescription(plugin).name}" }
},
onFailure = {
logger.info { "Cannot enable plugin ${plugin.description.name}" }
logger.info { "Cannot enable plugin ${getPluginDescription(plugin).name}" }
throw it
}
)
@ -148,7 +148,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
var count = 0
GlobalComponentStorage.run {
PluginLoaderProvider.useExtensions { ext, plugin ->
logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin.name}" }
logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin?.name ?: "<builtin>"}" }
_pluginLoaders.add(ext.instance)
count++
}
@ -166,7 +166,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
}
internal fun enableAllLoadedPlugins() {
resolvedPlugins.forEach { it.enable() }
resolvedPlugins.forEach { enablePlugin(it) }
}
@kotlin.jvm.Throws(PluginLoadException::class)
@ -180,7 +180,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
private fun List<PluginLoader<*, *>>.listAndSortAllPlugins(): List<PluginDescriptionWithLoader> {
return flatMap { loader ->
loader.listPlugins().map { plugin -> plugin.description.wrapWith(loader, plugin) }
loader.listPlugins().map { plugin -> getPluginDescription(plugin).wrapWith(loader, plugin) }
}.sortByDependencies()
}

View File

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

View File

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

View File

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

View File

@ -12,23 +12,22 @@
package net.mamoe.mirai.console.plugin
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.getPluginDescription
import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.SemVersion
import kotlin.DeprecationLevel.ERROR
/**
* 表示一个 mirai-console 插件.
*
* @see PluginManager.enable 启用一个插件
* @see PluginManager.disable 禁用一个插件
* @see PluginManager.enablePlugin 启用一个插件
* @see PluginManager.disablePlugin 禁用一个插件
* @see PluginManager.description 获取一个插件的 [描述][PluginDescription]
*
* @see PluginDescription 插件描述 需由 [PluginLoader] 帮助提供[PluginLoader.description]
* @see PluginDescription 插件描述 需由 [PluginLoader] 帮助提供[PluginLoader.getPluginDescription]
* @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件
* @see PluginFileExtensions 支持文件系统存储的扩展
*
@ -38,8 +37,8 @@ public interface Plugin : CommandOwner {
/**
* 判断此插件是否已启用
*
* @see PluginManager.enable 启用一个插件
* @see PluginManager.disable 禁用一个插件
* @see PluginManager.enablePlugin 启用一个插件
* @see PluginManager.disablePlugin 禁用一个插件
*/
public val isEnabled: Boolean
@ -49,32 +48,42 @@ public interface Plugin : CommandOwner {
public val loader: PluginLoader<*, *>
}
/**
* 获取 [PluginDescription]
*/
public inline val Plugin.description: PluginDescription get() = this.safeLoader.getPluginDescription(this)
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
@Deprecated(
"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]
*/
public inline val Plugin.version: SemVersion get() = this.description.version
public inline val Plugin.version: SemVersion get() = getPluginDescription(this).version
/**
* 获取 [PluginDescription.info]
*/
public inline val Plugin.info: String get() = this.description.info
public inline val Plugin.info: String get() = getPluginDescription(this).info
/**
* 获取 [PluginDescription.author]
*/
public inline val Plugin.author: String get() = this.description.author
public inline val Plugin.author: String get() = getPluginDescription(this).author
/**
* 获取 [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]
*/
public val Plugin.description: PluginDescription
public fun getPluginDescription(plugin: Plugin): PluginDescription
/**
* 禁用这个插件
*
* @see PluginLoader.disable
*/
public fun Plugin.disable(): Unit = safeLoader.disable(this)
public fun disablePlugin(plugin: Plugin): Unit = plugin.safeLoader.disable(plugin)
/**
* 加载这个插件
*
* @see PluginLoader.load
*/
public fun Plugin.load(): Unit = safeLoader.load(this)
public fun loadPlugin(plugin: Plugin): Unit = plugin.safeLoader.load(plugin)
/**
* 启用这个插件
*
* @see PluginLoader.enable
*/
public fun Plugin.enable(): Unit = safeLoader.enable(this)
public fun enablePlugin(plugin: Plugin): Unit = plugin.safeLoader.enable(plugin)
// endregion
public companion object INSTANCE : PluginManager by PluginManagerImpl {
/**
* 经过泛型类型转换的 [Plugin.loader]
*/
@get:JvmSynthetic
@Suppress("UNCHECKED_CAST")
public val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
public inline val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
get() = this.loader as PluginLoader<P, PluginDescription>
// endregion
public companion object INSTANCE : PluginManager by PluginManagerImpl {
// due to Kotlin's bug
public override val Plugin.description: PluginDescription get() = PluginManagerImpl.run { description }
public override fun Plugin.disable(): Unit = PluginManagerImpl.run { disable() }
public override fun Plugin.enable(): Unit = PluginManagerImpl.run { enable() }
public override fun Plugin.load(): Unit = PluginManagerImpl.run { load() }
public override val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription> get() = PluginManagerImpl.run { safeLoader }
@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] 类型版本.
*/
@ResolveContext(PLUGIN_VERSION)
@ResolveContext(SEMANTIC_VERSION)
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 fun permissionId(name: String): PermissionId =
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name, PermissionService.PluginPermissionIdRequestType.PERMISSION_ID)
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, name)
/**
* 重载 [PluginData]

View File

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

View File

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

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.util
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageContent
@ConsoleExperimentalApi
public object MessageUtils {
@JvmStatic
public fun MessageChain.messageContentsSequence(): Sequence<MessageContent> = asSequence().filterIsInstance<MessageContent>()
@JvmStatic
public fun MessageChain.firstContent(): MessageContent = messageContentsSequence().first()
@JvmStatic
public fun MessageChain.firstContentOrNull(): MessageContent? = messageContentsSequence().firstOrNull()
}

View File

@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.serializer
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.internal.data.map
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
@ -47,10 +47,10 @@ import kotlin.LazyThreadSafetyMode.PUBLICATION
* ```
* 其中 identifier metadata 都是可选的.
*
* 对于核心版本号, 此实现稍微比 semver 宽松一些, 允许 x.y 的存在.
* 对于核心版本号, 此实现稍微比语义化版本规范宽松一些, 允许 x.y 的存在.
*
* @see Requirement
* @see SemVersion.invoke
* @see Requirement 版本号要修
* @see SemVersion.invoke 由字符串解析
*/
@Serializable(with = SemVersion.SemVersionAsStringSerializer::class)
public data class SemVersion
@ -69,6 +69,15 @@ internal constructor(
/** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */
public val metadata: String? = null,
) : 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]
@ -103,10 +112,10 @@ internal constructor(
* - 如果不确定版本号是否合法, 可以使用 [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-]+)*))?$`
*/
@Throws(IllegalArgumentException::class, NumberFormatException::class)
@JvmStatic
@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]
@ -138,14 +147,15 @@ internal constructor(
* - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号
* - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()`
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun parseRangeRequirement(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Requirement =
SemVersionInternal.parseRangeRequirement(requirement)
/** @see [Requirement.test] */
@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
@ -157,6 +167,7 @@ internal constructor(
* 当满足 [requirement] 时返回 true, 否则返回 false
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun SemVersion.satisfies(@ResolveContext(VERSION_REQUIREMENT) requirement: String): Boolean = parseRangeRequirement(requirement).test(this)
/** for Kotlin only */
@ -167,7 +178,7 @@ internal constructor(
/** for Kotlin only */
@JvmStatic
@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
@ -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]
@ -194,27 +208,60 @@ internal constructor(
return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SemVersion
return compareTo(other) == 0 && other.identifier == identifier && other.metadata == metadata
/**
* 比较 `this` [other].
*
* @param deep `true` 时进行深度比较, 相当于 [equals]. `false` 时相当于 `compareTo(other) == 0`
* @see compareTo
*/
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
result *= (patch ?: 1)
/**
* 深度比较 `this` [other], 当且仅当 [major], [patch], [minor], [identifier], [metadata] 完全相同时返回 `true`.
*
* : `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 + (metadata?.hashCode() ?: 0)
return result
}
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
* 比较 `this` [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 {
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
@Suppress("UNCHECKED_CAST")
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R {
suspend fun <R> withTesting(timeout: Long = 50000L, block: suspend () -> Unit): R {
@Suppress("RemoveExplicitTypeArguments") // bug
return if (timeout != -1L) {
withTimeout<R>(timeout) {

View File

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

View File

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

View File

@ -22,7 +22,8 @@ internal class TestSemVersion {
internal fun testCompare() {
fun String.sem(): SemVersion = SemVersion.invoke(this)
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.0-M4".sem() < "1.0-M5".sem())
assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem())

View File

@ -8,8 +8,8 @@
*/
object Versions {
const val core = "1.3.0"
const val console = "1.0-RC-dev-31"
const val core = "1.3.2"
const val console = "1.0-RC-dev-32"
const val consoleGraphical = "0.0.7"
const val consoleTerminal = console
@ -19,7 +19,7 @@ object Versions {
const val coroutines = "1.3.9"
const val collectionsImmutable = "0.3.2"
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 androidGradle = "3.6.2"

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import net.mamoe.mirai.console.compiler.common.castOrNull
import net.mamoe.mirai.console.compiler.common.firstValue
import org.jetbrains.kotlin.descriptors.annotations.Annotated
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.constants.ArrayValue
import org.jetbrains.kotlin.resolve.constants.EnumValue
///////////////////////////////////////////////////////////////////////////
@ -73,11 +74,14 @@ enum class ResolveContextKind {
}
}
fun Annotated.isResolveContext(kind: ResolveContextKind) = this.resolveContextKind == kind
val Annotated.resolveContextKind: ResolveContextKind?
val Annotated.resolveContextKinds: List<ResolveContextKind>?
get() {
val ann = this.findAnnotation(RESOLVE_CONTEXT_FQ_NAME) ?: return null
val (_, enumEntryName) = ann.allValueArguments.firstValue().castOrNull<EnumValue>()?.value ?: return null // undetermined kind
return ResolveContextKind.valueOf(enumEntryName.asString())
val kinds =
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
internal object VersionConstants {
const val CONSOLE_VERSION = "1.0-RC-dev-30" // value is written here automatically during build
const val CORE_VERSION = "1.3.0" // 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.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 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.resolveContextKinds
import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls
import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues
import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments
@ -134,12 +134,18 @@ class ContextualParametersChecker : DeclarationChecker {
context: DeclarationCheckerContext,
) {
declaration.resolveAllCalls(context.bindingContext)
.asSequence()
.flatMap { call ->
call.valueParametersWithArguments().asSequence()
}
.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) ->
argument.resolveStringConstantValues()?.let { const ->
Triple(kind, argument, const)

View File

@ -31,12 +31,12 @@ class PluginDataValuesChecker : DeclarationChecker {
declaration.resolveAllCallsWithElement(bindingContext)
.filter { (call) -> call.isCalling(PLUGIN_DATA_VALUE_FUNCTIONS_FQ_FQ_NAME) }
.filter { (call) ->
call.resultingDescriptor.resolveContextKind == ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR
call.resultingDescriptor.resolveContextKinds?.contains(ResolveContextKind.RESTRICTED_NO_ARG_CONSTRUCTOR) == true
}.flatMap { (call, element) ->
call.typeArguments.entries.associateWith { element }.asSequence()
}.filter { (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
}.forEach { (e, callExpr) ->
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.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
fun DeclarationCheckerContext.report(diagnostic: Diagnostic) {
return this.trace.report(diagnostic)
@ -25,7 +24,6 @@ val DeclarationCheckerContext.bindingContext get() = this.trace.bindingContext
fun KtElement?.getResolvedCallOrResolveToCall(
context: DeclarationCheckerContext,
bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL,
): 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.ConstantValue
import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
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(
context: BindingContext,
bodyResolveMode: BodyResolveMode = BodyResolveMode.PARTIAL,
): 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