mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-11 02:50:15 +08:00
Rearrange implementations
This commit is contained in:
parent
d2194c2c2b
commit
2ba18ab7e4
@ -64,7 +64,7 @@ interface MiraiConsole {
|
|||||||
@MiraiExperimentalAPI
|
@MiraiExperimentalAPI
|
||||||
fun newLogger(identity: String?): MiraiLogger
|
fun newLogger(identity: String?): MiraiLogger
|
||||||
|
|
||||||
companion object INSTANCE : MiraiConsole by MiraiConsoleImpl
|
companion object INSTANCE : MiraiConsole by MiraiConsoleInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ internal object MiraiConsoleInitializer {
|
|||||||
/** 由前端调用 */
|
/** 由前端调用 */
|
||||||
internal fun init(instance: IMiraiConsole) {
|
internal fun init(instance: IMiraiConsole) {
|
||||||
this.instance = instance
|
this.instance = instance
|
||||||
MiraiConsoleImpl.initialize()
|
MiraiConsoleInternal.initialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mira
|
|||||||
/**
|
/**
|
||||||
* mirai 控制台实例.
|
* mirai 控制台实例.
|
||||||
*/
|
*/
|
||||||
internal object MiraiConsoleImpl : CoroutineScope, IMiraiConsole, MiraiConsole {
|
internal object MiraiConsoleInternal : CoroutineScope, IMiraiConsole, MiraiConsole {
|
||||||
override val pluginCenter: PluginCenter get() = CuiPluginCenter
|
override val pluginCenter: PluginCenter get() = CuiPluginCenter
|
||||||
|
|
||||||
private val instance: IMiraiConsole
|
private val instance: IMiraiConsole
|
||||||
|
@ -11,17 +11,15 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.console.command
|
package net.mamoe.mirai.console.command
|
||||||
|
|
||||||
import net.mamoe.mirai.console.command.description.*
|
|
||||||
import net.mamoe.mirai.message.data.PlainText
|
|
||||||
import net.mamoe.mirai.message.data.SingleMessage
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
import kotlin.reflect.KAnnotatedElement
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.full.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指令
|
* 指令
|
||||||
* 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand
|
* 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand
|
||||||
* @see register 注册这个指令
|
* @see register 注册这个指令
|
||||||
|
*
|
||||||
|
* @see SimpleCommand
|
||||||
|
* @see CompositeCommand
|
||||||
*/
|
*/
|
||||||
interface Command {
|
interface Command {
|
||||||
/**
|
/**
|
||||||
@ -30,6 +28,7 @@ interface Command {
|
|||||||
val names: Array<out String>
|
val names: Array<out String>
|
||||||
|
|
||||||
val usage: String
|
val usage: String
|
||||||
|
|
||||||
val description: String
|
val description: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,289 +46,12 @@ interface Command {
|
|||||||
/**
|
/**
|
||||||
* @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割.
|
* @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割.
|
||||||
*/
|
*/
|
||||||
suspend fun onCommand(sender: CommandSender, args: Array<out Any>)
|
suspend fun CommandSender.onCommand(args: Array<out Any>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend inline fun Command.onCommand(sender: CommandSender, args: Array<out Any>) = sender.run { onCommand(args) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主要指令名. 为 [Command.names] 的第一个元素.
|
* 主要指令名. 为 [Command.names] 的第一个元素.
|
||||||
*/
|
*/
|
||||||
val Command.primaryName: String get() = names[0]
|
val Command.primaryName: String get() = names[0]
|
||||||
|
|
||||||
/**
|
|
||||||
* 功能最集中的Commend
|
|
||||||
* 支持且只支持有sub的指令
|
|
||||||
* 例:
|
|
||||||
* /mute add
|
|
||||||
* /mute remove
|
|
||||||
* /mute addandremove (sub is case insensitive, lowercase are recommend)
|
|
||||||
* /mute add and remove('add and remove' consider as a sub)
|
|
||||||
*/
|
|
||||||
abstract class CompositeCommand @JvmOverloads constructor(
|
|
||||||
override val owner: CommandOwner,
|
|
||||||
vararg names: String,
|
|
||||||
description: String = "no description available",
|
|
||||||
override val permission: CommandPermission = CommandPermission.Default,
|
|
||||||
override val prefixOptional: Boolean = false,
|
|
||||||
overrideContext: CommandParserContext = EmptyCommandParserContext
|
|
||||||
) : Command {
|
|
||||||
override val description = description.trimIndent()
|
|
||||||
override val names: Array<out String> =
|
|
||||||
names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list ->
|
|
||||||
list.firstOrNull { !it.isValidSubName() }?.let {
|
|
||||||
error("Name is not valid: $it")
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [CommandArgParser] 的环境
|
|
||||||
*/
|
|
||||||
val context: CommandParserContext = CommandParserContext.Builtins + overrideContext
|
|
||||||
|
|
||||||
override var usage: String = "<command build failed>" // initialized by subCommand reflection
|
|
||||||
internal set
|
|
||||||
|
|
||||||
/** 指定子指令要求的权限 */
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
|
||||||
annotation class Permission(val permission: KClass<out CommandPermission>)
|
|
||||||
|
|
||||||
/** 标记一个函数为子指令 */
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
|
||||||
annotation class SubCommand(vararg val name: String)
|
|
||||||
|
|
||||||
/** 指令描述 */
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
|
||||||
annotation class Description(val description: String)
|
|
||||||
|
|
||||||
/** 参数名, 将参与构成 [usage] */
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
|
||||||
annotation class Name(val name: String)
|
|
||||||
|
|
||||||
final override suspend fun onCommand(sender: CommandSender, args: Array<out Any>) {
|
|
||||||
matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run {
|
|
||||||
defaultSubCommand.onCommand(sender, args)
|
|
||||||
}
|
|
||||||
subCommands
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
|
|
||||||
DefaultSubCommandDescriptor(
|
|
||||||
"",
|
|
||||||
CommandPermission.Default,
|
|
||||||
onCommand = block { sender: CommandSender, args: Array<out Any> ->
|
|
||||||
false//not supported yet
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class IllegalParameterException internal constructor(message: String) : Exception(message)
|
|
||||||
|
|
||||||
internal val subCommands: Array<SubCommandDescriptor> by lazy {
|
|
||||||
|
|
||||||
val buildUsage = StringBuilder(this.description).append(": \n")
|
|
||||||
|
|
||||||
this@CompositeCommand::class.declaredFunctions.filter { it.hasAnnotation<SubCommand>() }.map { function ->
|
|
||||||
|
|
||||||
val notStatic = function.findAnnotation<JvmStatic>()==null
|
|
||||||
val overridePermission = function.findAnnotation<Permission>()//optional
|
|
||||||
val subDescription = function.findAnnotation<Description>()?.description?:"no description available"
|
|
||||||
|
|
||||||
if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) {
|
|
||||||
throw IllegalParameterException("Return Type of SubCommand must be Boolean")
|
|
||||||
}
|
|
||||||
|
|
||||||
val parameter = function.parameters.toMutableList()
|
|
||||||
if (parameter.isEmpty()){
|
|
||||||
throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notStatic) {
|
|
||||||
parameter.removeAt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
(parameter.removeAt(0)).let {receiver ->
|
|
||||||
if (
|
|
||||||
receiver.isVararg ||
|
|
||||||
((receiver.type.classifier as? KClass<*>).also { print(it) }
|
|
||||||
?.isSubclassOf(CommandSender::class) != true)
|
|
||||||
) {
|
|
||||||
throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val commandName = function.findAnnotation<SubCommand>()!!.name.map {
|
|
||||||
if (!it.isValidSubName()) {
|
|
||||||
error("SubName $it is not valid")
|
|
||||||
}
|
|
||||||
it
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
//map parameter
|
|
||||||
val parms = parameter.map {
|
|
||||||
buildUsage.append("/$primaryName ")
|
|
||||||
|
|
||||||
if (it.isVararg) {
|
|
||||||
throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg")
|
|
||||||
}
|
|
||||||
if (it.isOptional) {
|
|
||||||
throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional")
|
|
||||||
}
|
|
||||||
|
|
||||||
val argName = it.findAnnotation<Name>()?.name ?: it.name ?: "unknown"
|
|
||||||
buildUsage.append("<").append(argName).append("> ").append(" ")
|
|
||||||
CommandParam(
|
|
||||||
argName,
|
|
||||||
(it.type.classifier as? KClass<*>)
|
|
||||||
?: throw IllegalParameterException("unsolved type reference from param " + it.name + " in " + function.name + " from " + this.primaryName)
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
buildUsage.append(subDescription).append("\n")
|
|
||||||
|
|
||||||
SubCommandDescriptor(
|
|
||||||
commandName,
|
|
||||||
parms,
|
|
||||||
subDescription,
|
|
||||||
overridePermission?.permission?.getInstance() ?: permission,
|
|
||||||
onCommand = block { sender: CommandSender, args: Array<out Any> ->
|
|
||||||
if (notStatic) {
|
|
||||||
function.callSuspend(this, sender, *args) as Boolean
|
|
||||||
} else {
|
|
||||||
function.callSuspend(sender, *args) as Boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}.toTypedArray().also {
|
|
||||||
usage = buildUsage.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> = 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(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() })
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inner class DefaultSubCommandDescriptor(
|
|
||||||
val description: String,
|
|
||||||
val permission: CommandPermission,
|
|
||||||
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
internal inner class SubCommandDescriptor(
|
|
||||||
val names: Array<String>,
|
|
||||||
val params: Array<CommandParam<*>>,
|
|
||||||
val description: String,
|
|
||||||
val permission: CommandPermission,
|
|
||||||
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean
|
|
||||||
) {
|
|
||||||
internal suspend inline fun parseAndExecute(
|
|
||||||
sender: CommandSender,
|
|
||||||
argsWithSubCommandNameNotRemoved: Array<out Any>
|
|
||||||
) {
|
|
||||||
if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) {
|
|
||||||
sender.sendMessage(usage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
|
|
||||||
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
|
|
||||||
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 String -> context[param.type]?.parse(rawArg, sender)
|
|
||||||
is SingleMessage -> context[param.type]?.parse(rawArg, sender)
|
|
||||||
else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}")
|
|
||||||
} ?: error("Cannot find a parser for $rawArg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException]
|
|
||||||
*/
|
|
||||||
internal fun matchSubCommand(rawArgs: Array<out Any>): SubCommandDescriptor? {
|
|
||||||
val maxCount = rawArgs.size - 1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
abstract class SimpleCommand(
|
|
||||||
override val owner: CommandOwner,
|
|
||||||
val name: String,
|
|
||||||
val alias: Array<String> = arrayOf()
|
|
||||||
) : Command {
|
|
||||||
abstract suspend fun CommandSender.onCommand(args: List<Any>)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class RawCommand(
|
|
||||||
final override val owner: CommandOwner,
|
|
||||||
name: String,
|
|
||||||
alias: Array<String> = arrayOf()
|
|
||||||
) : Command {
|
|
||||||
final override val names: Array<String> = arrayOf(name, *alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun <T> Array<T>.contentEqualsOffset(other: Array<out Any>, length: Int): Boolean {
|
|
||||||
repeat(length) { index ->
|
|
||||||
if (other[index].toString() != this[index]) {
|
|
||||||
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(): ArrayList<Any> {
|
|
||||||
val list = ArrayList<Any>()
|
|
||||||
when (this::class.java) { // faster than is
|
|
||||||
String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) }
|
|
||||||
PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() }
|
|
||||||
.forEach { list.add(it) }
|
|
||||||
SingleMessage::class.java -> list.add(this as SingleMessage)
|
|
||||||
Array<Any>::class.java -> (this as Array<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
|
|
||||||
Iterable::class.java -> (this as Iterable<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
|
|
||||||
else -> list.add(this.toString())
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
|
|
||||||
findAnnotation<T>() != null
|
|
||||||
|
|
||||||
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
|
|
||||||
return this.objectInstance ?: this.createInstance()
|
|
||||||
}
|
|
||||||
|
|
@ -18,6 +18,8 @@ package net.mamoe.mirai.console.command
|
|||||||
import kotlinx.atomicfu.locks.withLock
|
import kotlinx.atomicfu.locks.withLock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import net.mamoe.mirai.console.MiraiConsoleInternal
|
||||||
|
import net.mamoe.mirai.console.command.internal.*
|
||||||
import net.mamoe.mirai.console.plugin.Plugin
|
import net.mamoe.mirai.console.plugin.Plugin
|
||||||
import net.mamoe.mirai.message.data.Message
|
import net.mamoe.mirai.message.data.Message
|
||||||
import net.mamoe.mirai.message.data.MessageChain
|
import net.mamoe.mirai.message.data.MessageChain
|
||||||
@ -50,7 +52,11 @@ abstract class PluginCommandOwner(val plugin: Plugin) : CommandOwner() {
|
|||||||
*
|
*
|
||||||
* 由前端实现
|
* 由前端实现
|
||||||
*/
|
*/
|
||||||
internal abstract class ConsoleCommandOwner : CommandOwner()
|
internal abstract class ConsoleCommandOwner : CommandOwner() {
|
||||||
|
companion object {
|
||||||
|
internal val instance get() = MiraiConsoleInternal.consoleCommandOwner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
|
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
|
||||||
|
@ -13,18 +13,17 @@ package net.mamoe.mirai.console.command
|
|||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.mamoe.mirai.Bot
|
import net.mamoe.mirai.Bot
|
||||||
import net.mamoe.mirai.console.MiraiConsole
|
|
||||||
import net.mamoe.mirai.console.utils.JavaFriendlyAPI
|
import net.mamoe.mirai.console.utils.JavaFriendlyAPI
|
||||||
import net.mamoe.mirai.contact.*
|
import net.mamoe.mirai.contact.*
|
||||||
import net.mamoe.mirai.message.MessageEvent
|
import net.mamoe.mirai.message.MessageEvent
|
||||||
import net.mamoe.mirai.message.data.Message
|
import net.mamoe.mirai.message.data.Message
|
||||||
import net.mamoe.mirai.message.data.PlainText
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
import org.jetbrains.annotations.Contract
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指令发送者
|
* 指令发送者
|
||||||
*
|
*
|
||||||
* @see AbstractCommandSender 请继承于该抽象类
|
* @see ConsoleCommandSender
|
||||||
|
* @see UserCommandSender
|
||||||
*/
|
*/
|
||||||
@Suppress("FunctionName")
|
@Suppress("FunctionName")
|
||||||
interface CommandSender {
|
interface CommandSender {
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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("EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE")
|
||||||
|
|
||||||
|
package net.mamoe.mirai.console.command
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.command.description.CommandArgParser
|
||||||
|
import net.mamoe.mirai.console.command.description.CommandParserContext
|
||||||
|
import net.mamoe.mirai.console.command.description.EmptyCommandParserContext
|
||||||
|
import net.mamoe.mirai.console.command.description.plus
|
||||||
|
import net.mamoe.mirai.console.command.internal.CompositeCommandImpl
|
||||||
|
import net.mamoe.mirai.console.command.internal.isValidSubName
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能最集中的Commend
|
||||||
|
* 支持且只支持有sub的指令
|
||||||
|
* 例:
|
||||||
|
* /mute add
|
||||||
|
* /mute remove
|
||||||
|
* /mute addandremove (sub is case insensitive, lowercase are recommend)
|
||||||
|
* /mute add and remove('add and remove' consider as a sub)
|
||||||
|
*/
|
||||||
|
abstract class CompositeCommand @JvmOverloads constructor(
|
||||||
|
final override val owner: CommandOwner,
|
||||||
|
vararg names: String,
|
||||||
|
description: String = "no description available",
|
||||||
|
final override val permission: CommandPermission = CommandPermission.Default,
|
||||||
|
final override val prefixOptional: Boolean = false,
|
||||||
|
overrideContext: CommandParserContext = EmptyCommandParserContext
|
||||||
|
) : Command, CompositeCommandImpl() {
|
||||||
|
final override val description = description.trimIndent()
|
||||||
|
final override val names: Array<out String> =
|
||||||
|
names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list ->
|
||||||
|
list.firstOrNull { !it.isValidSubName() }?.let {
|
||||||
|
error("Name is not valid: $it")
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CommandArgParser] 的环境
|
||||||
|
*/
|
||||||
|
val context: CommandParserContext = CommandParserContext.Builtins + overrideContext
|
||||||
|
|
||||||
|
final override val usage: String get() = super.usage
|
||||||
|
|
||||||
|
/** 指定子指令要求的权限 */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
annotation class Permission(val permission: KClass<out CommandPermission>)
|
||||||
|
|
||||||
|
/** 标记一个函数为子指令 */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
annotation class SubCommand(vararg val name: String)
|
||||||
|
|
||||||
|
/** 指令描述 */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
annotation class Description(val description: String)
|
||||||
|
|
||||||
|
/** 参数名, 将参与构成 [usage] */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||||
|
annotation class Name(val name: String)
|
||||||
|
|
||||||
|
final override suspend fun onCommand(sender: CommandSender, args: Array<out Any>) {
|
||||||
|
matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run {
|
||||||
|
defaultSubCommand.onCommand(sender, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.mamoe.mirai.console.command
|
||||||
|
|
||||||
|
abstract class SimpleCommand(
|
||||||
|
override val owner: CommandOwner,
|
||||||
|
override vararg val names: String,
|
||||||
|
override val usage: String,
|
||||||
|
override val description: String,
|
||||||
|
override val permission: CommandPermission = CommandPermission.Default,
|
||||||
|
override val prefixOptional: Boolean = false
|
||||||
|
) : Command {
|
||||||
|
abstract override suspend fun CommandSender.onCommand(args: Array<out Any>)
|
||||||
|
}
|
@ -20,9 +20,11 @@ import kotlin.contracts.contract
|
|||||||
* this output type of that arg
|
* this output type of that arg
|
||||||
* input is always String
|
* input is always String
|
||||||
*/
|
*/
|
||||||
abstract class CommandArgParser<out T : Any> {
|
interface CommandArgParser<out T : Any> {
|
||||||
abstract fun parse(raw: String, sender: CommandSender): T
|
fun parse(raw: String, sender: CommandSender): T
|
||||||
open fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender)
|
|
||||||
|
@JvmDefault
|
||||||
|
fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> CommandArgParser<T>.parse(raw: Any, sender: CommandSender): T {
|
fun <T : Any> CommandArgParser<T>.parse(raw: Any, sender: CommandSender): T {
|
||||||
@ -61,7 +63,7 @@ inline fun CommandArgParser<*>.checkArgument(
|
|||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
inline fun <T : Any> CommandArgParser(
|
inline fun <T : Any> CommandArgParser(
|
||||||
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
||||||
): CommandArgParser<T> = object : CommandArgParser<T>() {
|
): CommandArgParser<T> = object : CommandArgParser<T> {
|
||||||
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
|
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,11 @@
|
|||||||
package net.mamoe.mirai.console.command.description
|
package net.mamoe.mirai.console.command.description
|
||||||
|
|
||||||
import net.mamoe.mirai.Bot
|
import net.mamoe.mirai.Bot
|
||||||
import net.mamoe.mirai.console.command.*
|
import net.mamoe.mirai.console.command.BotAwareCommandSender
|
||||||
|
import net.mamoe.mirai.console.command.CommandSender
|
||||||
|
import net.mamoe.mirai.console.command.MemberCommandSender
|
||||||
|
import net.mamoe.mirai.console.command.UserCommandSender
|
||||||
|
import net.mamoe.mirai.console.command.internal.fuzzySearchMember
|
||||||
import net.mamoe.mirai.contact.Friend
|
import net.mamoe.mirai.contact.Friend
|
||||||
import net.mamoe.mirai.contact.Group
|
import net.mamoe.mirai.contact.Group
|
||||||
import net.mamoe.mirai.contact.Member
|
import net.mamoe.mirai.contact.Member
|
||||||
|
@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* 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("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package net.mamoe.mirai.console.command.internal
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.command.*
|
||||||
|
import net.mamoe.mirai.console.command.description.CommandParam
|
||||||
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
|
import kotlin.reflect.KAnnotatedElement
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.*
|
||||||
|
|
||||||
|
internal abstract class CompositeCommandImpl : Command {
|
||||||
|
@JvmField
|
||||||
|
@Suppress("PropertyName")
|
||||||
|
internal var _usage: String = "<command build failed>"
|
||||||
|
|
||||||
|
override val usage: String // initialized by subCommand reflection
|
||||||
|
get() = _usage
|
||||||
|
|
||||||
|
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
|
||||||
|
DefaultSubCommandDescriptor(
|
||||||
|
"",
|
||||||
|
CommandPermission.Default,
|
||||||
|
onCommand = block { sender: CommandSender, args: Array<out Any> ->
|
||||||
|
false//not supported yet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val subCommands: Array<SubCommandDescriptor> by lazy {
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
this as CompositeCommand
|
||||||
|
|
||||||
|
val buildUsage = StringBuilder(this.description).append(": \n")
|
||||||
|
|
||||||
|
this::class.declaredFunctions.filter { it.hasAnnotation<CompositeCommand.SubCommand>() }.map { function ->
|
||||||
|
val notStatic = !function.hasAnnotation<JvmStatic>()
|
||||||
|
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
|
||||||
|
val subDescription =
|
||||||
|
function.findAnnotation<CompositeCommand.Description>()?.description ?: "no description available"
|
||||||
|
|
||||||
|
if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) {
|
||||||
|
error("Return Type of SubCommand must be Boolean")
|
||||||
|
}
|
||||||
|
|
||||||
|
val parameters = function.parameters.toMutableList()
|
||||||
|
check(parameters.isNotEmpty()) {
|
||||||
|
"First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notStatic) parameters.removeAt(0) // instance
|
||||||
|
|
||||||
|
(parameters.removeAt(0)).let { receiver ->
|
||||||
|
check(!receiver.isVararg && !((receiver.type.classifier as? KClass<*>).also { print(it) }
|
||||||
|
?.isSubclassOf(CommandSender::class) != true)) {
|
||||||
|
"First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be <out CommandSender>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val commandName = function.findAnnotation<CompositeCommand.SubCommand>()!!.name.map {
|
||||||
|
if (!it.isValidSubName()) {
|
||||||
|
error("SubName $it is not valid")
|
||||||
|
}
|
||||||
|
it
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
//map parameter
|
||||||
|
val params = parameters.map { param ->
|
||||||
|
buildUsage.append("/$primaryName ")
|
||||||
|
|
||||||
|
if (param.isVararg) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg")
|
||||||
|
if (param.isOptional) error("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional")
|
||||||
|
|
||||||
|
val argName = param.findAnnotation<CompositeCommand.Name>()?.name ?: param.name ?: "unknown"
|
||||||
|
buildUsage.append("<").append(argName).append("> ").append(" ")
|
||||||
|
CommandParam(
|
||||||
|
argName,
|
||||||
|
(param.type.classifier as? KClass<*>)
|
||||||
|
?: throw IllegalArgumentException("unsolved type reference from param " + param.name + " in " + function.name + " from " + this.primaryName)
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
buildUsage.append(subDescription).append("\n")
|
||||||
|
|
||||||
|
SubCommandDescriptor(
|
||||||
|
commandName,
|
||||||
|
params,
|
||||||
|
subDescription,
|
||||||
|
overridePermission?.permission?.getInstance() ?: permission,
|
||||||
|
onCommand = block { sender: CommandSender, args: Array<out Any> ->
|
||||||
|
if (notStatic) {
|
||||||
|
function.callSuspend(this, sender, *args) as Boolean
|
||||||
|
} else {
|
||||||
|
function.callSuspend(sender, *args) as Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.toTypedArray().also {
|
||||||
|
_usage = buildUsage.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
internal val bakedCommandNameToSubDescriptorArray: Map<Array<String>, SubCommandDescriptor> = 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(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() })
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultSubCommandDescriptor(
|
||||||
|
val description: String,
|
||||||
|
val permission: CommandPermission,
|
||||||
|
val onCommand: suspend (sender: CommandSender, rawArgs: Array<out Any>) -> Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
internal inner class SubCommandDescriptor(
|
||||||
|
val names: Array<String>,
|
||||||
|
val params: Array<CommandParam<*>>,
|
||||||
|
val description: String,
|
||||||
|
val permission: CommandPermission,
|
||||||
|
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean
|
||||||
|
) {
|
||||||
|
internal suspend inline fun parseAndExecute(
|
||||||
|
sender: CommandSender,
|
||||||
|
argsWithSubCommandNameNotRemoved: Array<out Any>
|
||||||
|
) {
|
||||||
|
if (!onCommand(sender, parseArgs(sender, argsWithSubCommandNameNotRemoved, names.size))) {
|
||||||
|
sender.sendMessage(usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
|
||||||
|
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
this as CompositeCommand
|
||||||
|
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 String -> context[param.type]?.parse(rawArg, sender)
|
||||||
|
is SingleMessage -> context[param.type]?.parse(rawArg, sender)
|
||||||
|
else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}")
|
||||||
|
} ?: error("Cannot find a parser for $rawArg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException]
|
||||||
|
*/
|
||||||
|
internal fun matchSubCommand(rawArgs: Array<out Any>): SubCommandDescriptor? {
|
||||||
|
val maxCount = rawArgs.size - 1
|
||||||
|
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: Array<out Any>, length: Int): Boolean {
|
||||||
|
repeat(length) { index ->
|
||||||
|
if (other[index].toString() != this[index]) {
|
||||||
|
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(): ArrayList<Any> {
|
||||||
|
val list = ArrayList<Any>()
|
||||||
|
when (this::class.java) { // faster than is
|
||||||
|
String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) }
|
||||||
|
PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() }
|
||||||
|
.forEach { list.add(it) }
|
||||||
|
SingleMessage::class.java -> list.add(this as SingleMessage)
|
||||||
|
Array<Any>::class.java -> (this as Array<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
|
||||||
|
Iterable::class.java -> (this as Iterable<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) }
|
||||||
|
else -> list.add(this.toString())
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
|
||||||
|
findAnnotation<T>() != null
|
||||||
|
|
||||||
|
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
|
||||||
|
return this.objectInstance ?: this.createInstance()
|
||||||
|
}
|
@ -7,8 +7,9 @@
|
|||||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package net.mamoe.mirai.console.command
|
package net.mamoe.mirai.console.command.internal
|
||||||
|
|
||||||
|
import net.mamoe.mirai.console.command.*
|
||||||
import net.mamoe.mirai.contact.Group
|
import net.mamoe.mirai.contact.Group
|
||||||
import net.mamoe.mirai.contact.Member
|
import net.mamoe.mirai.contact.Member
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
@ -51,7 +52,9 @@ internal object InternalCommandManager {
|
|||||||
*/
|
*/
|
||||||
internal fun matchCommand(rawCommand: String): Command? {
|
internal fun matchCommand(rawCommand: String): Command? {
|
||||||
if (rawCommand.startsWith(COMMAND_PREFIX)) {
|
if (rawCommand.startsWith(COMMAND_PREFIX)) {
|
||||||
return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX)]
|
return requiredPrefixCommandMap[rawCommand.substringAfter(
|
||||||
|
COMMAND_PREFIX
|
||||||
|
)]
|
||||||
}
|
}
|
||||||
return optionalPrefixCommandMap[rawCommand]
|
return optionalPrefixCommandMap[rawCommand]
|
||||||
}
|
}
|
||||||
@ -170,10 +173,16 @@ internal suspend inline fun CommandSender.executeCommandInternal(
|
|||||||
messages: Any,
|
messages: Any,
|
||||||
commandName: String
|
commandName: String
|
||||||
): Command? {
|
): Command? {
|
||||||
val command = InternalCommandManager.matchCommand(commandName) ?: return null
|
val command = InternalCommandManager.matchCommand(
|
||||||
|
commandName
|
||||||
|
) ?: return null
|
||||||
|
|
||||||
if (!command.testPermission(this)) {
|
if (!command.testPermission(this)) {
|
||||||
throw CommandExecutionException(command, commandName, CommandPermissionDeniedException(command))
|
throw CommandExecutionException(
|
||||||
|
command,
|
||||||
|
commandName,
|
||||||
|
CommandPermissionDeniedException(command)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
@ -9,16 +9,12 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.console.event
|
package net.mamoe.mirai.console.event
|
||||||
|
|
||||||
import net.mamoe.mirai.console.command.Command
|
/*
|
||||||
import net.mamoe.mirai.console.command.CommandSender
|
|
||||||
import net.mamoe.mirai.event.AbstractEvent
|
|
||||||
import net.mamoe.mirai.event.CancellableEvent
|
|
||||||
|
|
||||||
data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent
|
data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent
|
||||||
val sender: CommandSender,
|
val sender: CommandSender,
|
||||||
val command: Command,
|
val command: Command,
|
||||||
val rawArgs: Array<Any>
|
val rawArgs: Array<Any>
|
||||||
) : AbstractEvent(), CancellableEvent, ConsoleEvent {
|
) : CancellableEvent, ConsoleEvent, AbstractEvent() {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@ -39,3 +35,4 @@ data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
@ -27,6 +27,9 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||||||
|
|
||||||
internal val <T> T.job: Job where T : CoroutineScope, T : Plugin get() = this.coroutineContext[Job]!!
|
internal val <T> T.job: Job where T : CoroutineScope, T : Plugin get() = this.coroutineContext[Job]!!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides implementations from [JvmPlugin]
|
||||||
|
*/
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal abstract class JvmPluginImpl(
|
internal abstract class JvmPluginImpl(
|
||||||
parentCoroutineContext: CoroutineContext
|
parentCoroutineContext: CoroutineContext
|
||||||
|
@ -25,4 +25,6 @@ abstract class AbstractJvmPlugin @JvmOverloads constructor(
|
|||||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||||
) : JvmPlugin, JvmPluginImpl(parentCoroutineContext) {
|
) : JvmPlugin, JvmPluginImpl(parentCoroutineContext) {
|
||||||
// TODO: 2020/6/24 添加 PluginSetting 继承 Setting, 实现 onValueChanged 并绑定自动保存.
|
// TODO: 2020/6/24 添加 PluginSetting 继承 Setting, 实现 onValueChanged 并绑定自动保存.
|
||||||
|
|
||||||
|
abstract class PluginSetting
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user