Command registering, matching, executing

This commit is contained in:
Him188 2020-05-14 15:33:10 +08:00
parent 41112affa8
commit ee8be9799f
19 changed files with 520 additions and 271 deletions

View File

@ -40,6 +40,8 @@ dependencies {
testApi("net.mamoe:mirai-core-qqandroid:${Versions.Mirai.core}")
testApi(kotlin("stdlib"))
testApi(kotlin("test"))
testApi(kotlin("test-junit5"))
}
version = Versions.Mirai.console

View File

@ -1,16 +0,0 @@
package net.mamoe.mirai.console.command;
// import jdk.jfr.Description;
public class JCommandManager {
private JCommandManager() {
}
public static CommandManager getInstance() {
return CommandManager.INSTANCE;
}
}

View File

@ -1,7 +1,6 @@
package net.mamoe.mirai.console.utils;
import net.mamoe.mirai.Bot;
import net.mamoe.mirai.console.MiraiConsole;
import java.util.List;
@ -12,7 +11,7 @@ import java.util.List;
public class BotManager {
public static List<Long> getManagers(long botAccount) {
Bot bot = MiraiConsole.INSTANCE.getBotOrThrow(botAccount);
Bot bot = Bot.getInstance(botAccount);
return getManagers(bot);
}

View File

@ -13,9 +13,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.io.charsets.Charset
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.plugins.PluginManager
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiLogger
import java.io.ByteArrayOutputStream
import java.io.PrintStream
@ -42,7 +43,7 @@ interface IMiraiConsole : CoroutineScope {
val mainLogger: MiraiLogger
}
object MiraiConsole : CoroutineScope, IMiraiConsole {
object MiraiConsole : CoroutineScope, IMiraiConsole, CommandOwner {
private lateinit var instance: IMiraiConsole
/** 由前端调用 */
@ -58,18 +59,20 @@ object MiraiConsole : CoroutineScope, IMiraiConsole {
override val coroutineContext: CoroutineContext get() = instance.coroutineContext
init {
DefaultLogger = {
this.newLogger(it)
}
this.coroutineContext[Job]!!.invokeOnCompletion {
PluginManager.disablePlugins()
CommandManager.cancel()
Bot.botInstances.forEach {
it.close()
}
}
}
@MiraiExperimentalAPI
fun newLogger(identity: String?): MiraiLogger = frontEnd.loggerFor(identity)
}
internal val Throwable.stacktraceString: String
get() =
ByteArrayOutputStream().apply {

View File

@ -7,15 +7,8 @@ import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.plugins.PluginManager
import net.mamoe.mirai.console.utils.retryCatching
import net.mamoe.mirai.console.utils.tryNTimes
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
internal object CuiPluginCenter : PluginCenter {
@ -96,6 +89,8 @@ internal object CuiPluginCenter : PluginCenter {
}
override suspend fun <T : Any> T.downloadPlugin(name: String, progressListener: T.(Float) -> Unit): File {
TODO()
/*
val info = findPlugin(name) ?: error("Plugin Not Found")
val targetFile = File(PluginManager.pluginsPath, "$name-" + info.version + ".jar")
withContext(Dispatchers.IO) {
@ -116,6 +111,7 @@ internal object CuiPluginCenter : PluginCenter {
}
}
return targetFile
*/
}
override val name: String

View File

@ -13,129 +13,134 @@ package net.mamoe.mirai.console.command
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.plugins.PluginBase
import kotlin.reflect.KProperty
internal const val FOR_BINARY_COMPATIBILITY = "for binary compatibility"
/**
* 指令
*
* @see register 注册这个指令
* @see registerCommand 注册指令 DSL
*/
interface Command {
/**
* 指令主名称
*/
val name: String
val owner: CommandOwner
val descriptor: CommandDescriptor
/*
@Deprecated(FOR_BINARY_COMPATIBILITY, level = DeprecationLevel.HIDDEN)
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
return true
}*/
/**
* 别名
* 执行这个指令.
*/
val alias: List<String>
/**
* 描述, 将会显示在 "/help" 指令中
*/
val description: String
/**
* 用法说明
*/
val usage: String
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean
}
abstract class AbstractCommand(
override val name: String,
override val alias: List<String>,
override val description: String,
override val usage: String
) : Command
/**
* 注册这个指令
*/
inline fun Command.register(commandOwner: CommandOwner) = CommandManager.register(commandOwner, this)
internal inline fun registerConsoleCommands(builder: CommandBuilder.() -> Unit): Command {
return CommandBuilder().apply(builder).register(ConsoleCommandOwner)
suspend fun onCommand(sender: CommandSender, args: CommandArgs): Boolean
}
/**
* 构造并注册一个指令
* 指令实际参数列表. 参数顺序与 [Command.descriptor] [CommandDescriptor.params] 相同.
*/
inline fun PluginBase.registerCommand(builder: CommandBuilder.() -> Unit): Command {
return CommandBuilder().apply(builder).register(this.asCommandOwner())
}
// for java
@Suppress("unused")
abstract class BlockingCommand(
override val name: String,
override val alias: List<String> = listOf(),
override val description: String = "",
override val usage: String = ""
) : Command {
class CommandArgs private constructor(
@JvmField
internal val values: List<Any>,
private val fromCommand: Command
) : List<Any> by values {
/**
* 最高优先级监听器.
* 获取第一个类型为 [R] 的参数
*/
@JvmSynthetic
inline fun <reified R> getReified(): R {
for (value in this) {
if (value is R) {
return value
}
}
error("Cannot find argument typed ${R::class.qualifiedName}")
}
/**
* 获取名称为 [name] 的参数.
*
* 指令调用将优先触发 [Command.onCommand], 若该函数返回 `false`, 则不会调用 [PluginBase.onCommand]
* */
final override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
return withContext(Dispatchers.IO) {
onCommandBlocking(sender, args)
* [name] `null` 则获取第一个匿名参数
* @throws NoSuchElementException 找不到这个名称的参数时抛出
*/
operator fun get(name: String?): Any {
val index = fromCommand.descriptor.params.indexOfFirst { it.name == name }
if (index == -1) {
throw NoSuchElementException("Cannot find argument named $name")
}
return values[index]
}
/**
* 获取名称为 [name] 的参数. 并强转为 [R].
*
* [name] `null` 则获取第一个匿名参数
* @throws IllegalStateException 无法强转时抛出
*/
fun <R> getAs(name: String?): R {
@Suppress("UNCHECKED_CAST")
return this[name] as? R ?: error("Argument $name has a type $")
}
/** 获取第一个类型为 [R] 的参数并提供委托 */
inline operator fun <reified R : Any> getValue(thisRef: Any?, property: KProperty<*>): R = getReified()
companion object {
fun parseFrom(command: Command, sender: CommandSender, rawArgs: List<Any>): CommandArgs {
val params = command.descriptor.params
require(rawArgs.size >= params.size) { "No enough rawArgs: required ${params.size}, found only ${rawArgs.size}" }
command.descriptor.params.asSequence().zip(rawArgs.asSequence()).map { (commandParam, any) ->
command.parserFor(commandParam)?.parse(any, sender)
?: error("ICould not find a parser for param named ${commandParam.name}")
}.toList().let { bakedArgs ->
return CommandArgs(bakedArgs, command)
}
}
}
abstract fun onCommandBlocking(sender: CommandSender, args: List<String>): Boolean
}
inline val Command.fullName get() = descriptor.fullName
inline val Command.usage get() = descriptor.usage
inline val Command.params get() = descriptor.params
inline val Command.description get() = descriptor.description
inline val Command.context get() = descriptor.context
inline val Command.aliases get() = descriptor.aliases
inline val Command.permission get() = descriptor.permission
inline val Command.allNames get() = descriptor.allNames
abstract class PluginCommand(
final override val owner: PluginBase,
descriptor: CommandDescriptor
) : AbstractCommand(descriptor)
internal abstract class ConsoleCommand(
descriptor: CommandDescriptor
) : AbstractCommand(descriptor) {
final override val owner: MiraiConsole get() = MiraiConsole
}
sealed class AbstractCommand(
final override val descriptor: CommandDescriptor
) : Command
/**
* @see registerCommand
* For Java
*/
class CommandBuilder @PublishedApi internal constructor() {
var name: String? = null
var alias: List<String>? = null
var description: String = ""
var usage: String = "use /help for help"
internal var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> Boolean) {
onCommand = commandProcess
@Suppress("unused")
abstract class BlockingCommand(
owner: PluginBase,
descriptor: CommandDescriptor
) : PluginCommand(owner, descriptor) {
final override suspend fun onCommand(sender: CommandSender, args: CommandArgs): Boolean {
return withContext(Dispatchers.IO) { onCommandBlocking(sender, args) }
}
}
// internal
internal class AnonymousCommand internal constructor(
override val name: String,
override val alias: List<String>,
override val description: String,
override val usage: String = "",
val onCommand: suspend CommandSender.(args: List<String>) -> Boolean
) : Command {
override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
return onCommand.invoke(sender, args)
}
}
@PublishedApi
internal fun CommandBuilder.register(commandOwner: CommandOwner): AnonymousCommand {
if (name == null || onCommand == null) {
error("CommandBuilder not complete")
}
if (alias == null) {
alias = listOf()
}
return AnonymousCommand(
name!!,
alias!!,
description,
usage,
onCommand!!
).also { it.register(commandOwner) }
abstract fun onCommandBlocking(sender: CommandSender, args: CommandArgs): Boolean
}

View File

@ -17,8 +17,20 @@ import kotlin.contracts.contract
* input is always String
*/
abstract class CommandArgParser<out T : Any> {
abstract fun parse(s: String, sender: CommandSender): T
open fun parse(s: SingleMessage, sender: CommandSender): T = parse(s.content, sender)
abstract fun parse(raw: String, sender: CommandSender): T
open fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender)
}
fun <T : Any> CommandArgParser<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}")
}
}
@Suppress("unused")
@ -46,7 +58,7 @@ inline fun CommandArgParser<*>.checkArgument(
inline fun <T : Any> CommandArgParser(
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
): CommandArgParser<T> = object : CommandArgParser<T>() {
override fun parse(s: String, sender: CommandSender): T = parser(s, sender)
override fun parse(raw: String, sender: CommandSender): T = parser(raw, sender)
}
@ -57,41 +69,41 @@ class ParserException(message: String, cause: Throwable? = null) : RuntimeExcept
object IntArgParser : CommandArgParser<Int>() {
override fun parse(s: String, sender: CommandSender): Int =
s.toIntOrNull() ?: illegalArgument("无法解析 $s 为整数")
override fun parse(raw: String, sender: CommandSender): Int =
raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数")
}
object LongArgParser : CommandArgParser<Long>() {
override fun parse(s: String, sender: CommandSender): Long =
s.toLongOrNull() ?: illegalArgument("无法解析 $s 为长整数")
override fun parse(raw: String, sender: CommandSender): Long =
raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数")
}
object ShortArgParser : CommandArgParser<Short>() {
override fun parse(s: String, sender: CommandSender): Short =
s.toShortOrNull() ?: illegalArgument("无法解析 $s 为短整数")
override fun parse(raw: String, sender: CommandSender): Short =
raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数")
}
object ByteArgParser : CommandArgParser<Byte>() {
override fun parse(s: String, sender: CommandSender): Byte =
s.toByteOrNull() ?: illegalArgument("无法解析 $s 为字节")
override fun parse(raw: String, sender: CommandSender): Byte =
raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节")
}
object DoubleArgParser : CommandArgParser<Double>() {
override fun parse(s: String, sender: CommandSender): Double =
s.toDoubleOrNull() ?: illegalArgument("无法解析 $s 为小数")
override fun parse(raw: String, sender: CommandSender): Double =
raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
object FloatArgParser : CommandArgParser<Float>() {
override fun parse(s: String, sender: CommandSender): Float =
s.toFloatOrNull() ?: illegalArgument("无法解析 $s 为小数")
override fun parse(raw: String, sender: CommandSender): Float =
raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数")
}
object StringArgParser : CommandArgParser<String>() {
override fun parse(s: String, sender: CommandSender): String = s
override fun parse(raw: String, sender: CommandSender): String = raw
}
object BooleanArgParser : CommandArgParser<Boolean>() {
override fun parse(s: String, sender: CommandSender): Boolean = s.trim().let { str ->
override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true)
|| str.equals("enabled", ignoreCase = true)
@ -105,11 +117,11 @@ object BooleanArgParser : CommandArgParser<Boolean>() {
* errors: String->Int convert, Bot Not Exist
*/
object ExistBotArgParser : CommandArgParser<Bot>() {
override fun parse(s: String, sender: CommandSender): Bot {
override fun parse(raw: String, sender: CommandSender): Bot {
val uin = try {
s.toLong()
raw.toLong()
} catch (e: Exception) {
error("无法识别QQ UIN$s")
error("无法识别QQ UIN$raw")
}
return try {
Bot.getInstance(uin)
@ -123,8 +135,8 @@ object ExistFriendArgParser : CommandArgParser<Friend>() {
//Bot.friend
//friend
//~ = self
override fun parse(s: String, sender: CommandSender): Friend {
if (s == "~") {
override fun parse(raw: String, sender: CommandSender): Friend {
if (raw == "~") {
if (sender !is BotAware) {
illegalArgument("无法解析~作为默认")
}
@ -141,20 +153,20 @@ object ExistFriendArgParser : CommandArgParser<Friend>() {
}
if (sender is BotAware) {
return try {
sender.bot.friends[s.toLong()]
sender.bot.friends[raw.toLong()]
} catch (e: NoSuchElementException) {
error("无法找到" + s + "这个好友")
error("无法找到" + raw + "这个好友")
} catch (e: NumberFormatException) {
error("无法解析$s")
error("无法解析$raw")
}
} else {
s.split(".").let { args ->
raw.split(".").let { args ->
if (args.size != 2) {
illegalArgument("无法解析 $s, 格式应为 机器人账号.好友账号")
illegalArgument("无法解析 $raw, 格式应为 机器人账号.好友账号")
}
return try {
Bot.getInstance(args[0].toLong()).friends.getOrNull(
args[1].toLongOrNull() ?: illegalArgument("无法解析 $s 为好友")
args[1].toLongOrNull() ?: illegalArgument("无法解析 $raw 为好友")
) ?: illegalArgument("无法找到好友 ${args[1]}")
} catch (e: NoSuchElementException) {
illegalArgument("无法找到机器人账号 ${args[0]}")
@ -163,28 +175,28 @@ object ExistFriendArgParser : CommandArgParser<Friend>() {
}
}
override fun parse(s: SingleMessage, sender: CommandSender): Friend {
if (s is At) {
override fun parse(raw: SingleMessage, sender: CommandSender): Friend {
if (raw is At) {
assert(sender is GroupContactCommandSender)
return (sender as BotAware).bot.friends.getOrNull(s.target) ?: illegalArgument("At的对象非Bot好友")
return (sender as BotAware).bot.friends.getOrNull(raw.target) ?: illegalArgument("At的对象非Bot好友")
} else {
error("无法解析 $s 为好友")
error("无法解析 $raw 为好友")
}
}
}
object ExistGroupArgParser : CommandArgParser<Group>() {
override fun parse(s: String, sender: CommandSender): Group {
override fun parse(raw: String, sender: CommandSender): Group {
//by default
if ((s == "" || s == "~") && sender is GroupContactCommandSender) {
if ((raw == "" || raw == "~") && sender is GroupContactCommandSender) {
return sender.contact as Group
}
//from bot to group
if (sender is BotAware) {
val code = try {
s.toLong()
raw.toLong()
} catch (e: NoSuchElementException) {
error("无法识别Group Code$s")
error("无法识别Group Code$raw")
}
return try {
sender.bot.getGroup(code)
@ -193,7 +205,7 @@ object ExistGroupArgParser : CommandArgParser<Group>() {
}
}
//from console/other
return with(s.split(".")) {
return with(raw.split(".")) {
if (this.size != 2) {
error("请使用BotQQ号.群号 来表示Bot的一个群")
}
@ -213,9 +225,9 @@ object ExistMemberArgParser : CommandArgParser<Member>() {
//私聊: Group.Member[QQ/名片]
//群内: Q号
//群内: 名片
override fun parse(s: String, sender: CommandSender): Member {
override fun parse(raw: String, sender: CommandSender): Member {
if (sender !is BotAware) {
with(s.split(".")) {
with(raw.split(".")) {
checkArgument(this.size >= 3) {
"无法识别Member, 请使用Bot.Group.Member[QQ/名片]的格式"
}
@ -246,12 +258,12 @@ object ExistMemberArgParser : CommandArgParser<Member>() {
if (sender is GroupContactCommandSender) {
val group = sender.contact as Group
return try {
group.members[s.toLong()]
group.members[raw.toLong()]
} catch (ignored: Exception) {
group.fuzzySearchMember(s) ?: illegalArgument("无法找到成员$s")
group.fuzzySearchMember(raw) ?: illegalArgument("无法找到成员$raw")
}
} else {
with(s.split(".")) {
with(raw.split(".")) {
if (this.size < 2) {
illegalArgument("无法识别Member, 请使用Group.Member[QQ/名片]的格式")
}
@ -274,12 +286,12 @@ object ExistMemberArgParser : CommandArgParser<Member>() {
}
}
override fun parse(s: SingleMessage, sender: CommandSender): Member {
return if (s is At) {
override fun parse(raw: SingleMessage, sender: CommandSender): Member {
return if (raw is At) {
checkArgument(sender is GroupContactCommandSender)
(sender.contact as Group).members[s.target]
(sender.contact as Group).members[raw.target]
} else {
illegalArgument("无法识别Member" + s.content)
illegalArgument("无法识别Member" + raw.content)
}
}
}

View File

@ -2,6 +2,10 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage
import kotlin.reflect.KClass
/**
@ -11,15 +15,19 @@ class CommandDescriptor(
/**
* 包含子命令的全名. "`group kick`", 其中 `kick` `group` 的子命令
*/
val fullName: String,
fullName: CommandFullName,
/**
* 用法说明
*/
val usage: String,
usage: String,
/**
* 指令参数列表, 有顺序.
*/
val params: List<CommandParam<*>>,
/**
* 指令说明
*/
description: String = "",
/**
* 指令参数解析器环境.
*/
@ -27,7 +35,7 @@ class CommandDescriptor(
/**
* 指令别名
*/
val aliases: Array<String> = arrayOf(),
aliases: Array<CommandFullName> = arrayOf(),
/**
* 指令权限
*
@ -35,19 +43,101 @@ class CommandDescriptor(
* @see CommandPermission.and 同时要求两个权限
*/
val permission: CommandPermission = CommandPermission.Default
)
) {
/**
* 指令别名
*/
val aliases: Array<CommandFullName> = aliases.map { it.checkFullName("alias") }.toTypedArray()
/**
* 指令说明
*/
val description: String = description.trim()
/**
* 用法说明
*/
val usage: String = usage.trim()
/**
* 包含子命令的全名. "`group kick`", 其中 `kick` `group` 的子命令
* 元素类型可以为 [Message] [String]
*/
val fullName: CommandFullName = fullName.checkFullName("fullName")
/**
* `fullName + aliases`
*/
val allNames = arrayOf(fullName, *aliases)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CommandDescriptor
if (!fullName.contentEquals(other.fullName)) return false
if (usage != other.usage) return false
if (params != other.params) return false
if (description != other.description) return false
if (context != other.context) return false
if (!aliases.contentEquals(other.aliases)) return false
if (permission != other.permission) return false
return true
}
override fun hashCode(): Int {
var result = fullName.hashCode()
result = 31 * result + usage.hashCode()
result = 31 * result + params.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + context.hashCode()
result = 31 * result + aliases.contentHashCode()
result = 31 * result + permission.hashCode()
return result
}
}
fun Command.checkArgs(args: CommandArgs) = this.descriptor.checkArgs(args)
fun CommandDescriptor.checkArgs(args: CommandArgs) {
require(args.size >= this.params.size) { "No enough args. Required ${params.size}, but given ${args.size}" }
params.forEachIndexed { index, commandParam ->
require(commandParam.type.isInstance(args[index])) {
"Illegal arg #$index, required ${commandParam.type.qualifiedName}, but given ${args[index]::class.qualifiedName}"
}
}
}
internal fun Any.flattenCommandComponents(): Sequence<Any> = when (this) {
is Array<*> -> this.asSequence().flatMap {
it?.flattenCommandComponents() ?: throw java.lang.IllegalArgumentException("unexpected null value")
}
is String -> splitToSequence(' ').filterNot { it.isBlank() }
is PlainText -> content.flattenCommandComponents()
is SingleMessage -> sequenceOf(this)
is MessageChain -> this.asSequence().map { it.flattenCommandComponents() }
else -> throw IllegalArgumentException("Illegal component: $this")
}
internal fun CommandFullName.checkFullName(errorHint: String): CommandFullName {
return flattenCommandComponents().toList().also {
require(it.isNotEmpty()) { "$errorHint must not be empty" }
}.toTypedArray()
}
/**
* 构建一个 [CommandDescriptor]
*/
@Suppress("FunctionName")
inline fun CommandDescriptor(
fullName: String,
block: CommandDescriptorBuilder.() -> Unit
vararg fullName: Any,
block: CommandDescriptorBuilder.() -> Unit = {}
): CommandDescriptor = CommandDescriptorBuilder(fullName).apply(block).build()
class CommandDescriptorBuilder(
val fullName: String
vararg val fullName: Any
) {
@PublishedApi
internal var context: CommandParserContext = CommandParserContext.Builtins
@ -62,7 +152,10 @@ class CommandDescriptorBuilder(
internal var usage: String = "<no usage>"
@PublishedApi
internal var aliases: MutableList<String> = mutableListOf()
internal var aliases: MutableList<CommandFullName> = mutableListOf()
@PublishedApi
internal var description: String = ""
/** 增加指令参数解析器列表 */
@JvmSynthetic
@ -91,8 +184,15 @@ class CommandDescriptorBuilder(
usage = message
}
fun alias(vararg name: String): CommandDescriptorBuilder = apply {
this.aliases.addAll(name)
fun description(description: String): CommandDescriptorBuilder = apply {
this.description = description
}
/**
* 添加一个别名
*/
fun alias(vararg fullName: Any): CommandDescriptorBuilder = apply {
this.aliases.add(fullName)
}
fun param(vararg params: CommandParam<*>): CommandDescriptorBuilder = apply {
@ -105,7 +205,7 @@ class CommandDescriptorBuilder(
type: KClass<T>,
overrideParser: CommandArgParser<T>? = null
): CommandDescriptorBuilder = apply {
this.params.add(CommandParam(name, type).apply { this.parser = overrideParser })
this.params.add(CommandParam(name, type).apply { this._overrideParser = overrideParser })
}
fun <T : Any> param(
@ -143,7 +243,7 @@ class CommandDescriptorBuilder(
}
fun build(): CommandDescriptor =
CommandDescriptor(fullName, context, params, usage, aliases.toTypedArray(), permission)
CommandDescriptor(fullName, usage, params, description, context, aliases.toTypedArray(), permission)
}
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
@ -154,7 +254,7 @@ inline class ParamBlock internal constructor(@PublishedApi internal val list: Mu
/** 指定 [CommandParam.overrideParser] */
infix fun <T : Any> CommandParam<T>.using(parser: CommandArgParser<T>): CommandParam<T> =
this.apply { this.parser = parser }
this.apply { this._overrideParser = parser }
/** 覆盖 [CommandArgParser] */
inline infix fun <reified T : Any> String.using(parser: CommandArgParser<T>): CommandParam<T> =

View File

@ -1,3 +1,121 @@
@file:Suppress("NOTHING_TO_INLINE")
@file:JvmName("CommandManager")
package net.mamoe.mirai.console.command
object CommandManager
import kotlinx.atomicfu.locks.withLock
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import java.util.*
import java.util.concurrent.locks.ReentrantLock
typealias CommandFullName = Array<out Any>
interface CommandOwner
val CommandOwner.registeredCommands: List<Command> get() = InternalCommandManager.registeredCommands.filter { it.owner == this }
fun CommandOwner.unregisterAllCommands() {
for (registeredCommand in registeredCommands) {
registeredCommand.unregister()
}
}
/**
* 注册一个指令. 若此指令已经注册或有已经注册的指令与 [allNames] 重名, 返回 `false`
*/
fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock {
with(descriptor) {
if (findDuplicate() != null) {
return false
}
InternalCommandManager.registeredCommands.add(this@register)
for (name in this.allNames) {
InternalCommandManager.nameToCommandMap[name] = this@register
}
return true
}
}
/**
* 查找是否有重名的指令. 返回重名的指令.
*/
fun Command.findDuplicate(): Command? {
return InternalCommandManager.nameToCommandMap.entries.firstOrNull { (names, _) ->
this.allNames.any { it.contentEquals(names) }
}?.value
}
/**
* 取消注册这个指令. 若指令未注册, 返回 `false`
*/
fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock {
if (!InternalCommandManager.registeredCommands.contains(this)) {
return false
}
InternalCommandManager.registeredCommands.remove(this)
for (name in this.allNames) {
InternalCommandManager.nameToCommandMap.entries.removeIf {
it.key.contentEquals(this.fullName)
}
}
return true
}
/**
* 解析并执行一个指令
* @param args 接受 [String] [Message]
* @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配
*/
suspend fun CommandSender.executeCommand(vararg args: Any): Boolean {
return args.flattenCommandComponents().toList().executeCommand(this)
}
/**
* 解析并执行一个指令
* @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配
*/
suspend fun MessageChain.executeAsCommand(sender: CommandSender): Boolean {
return this.flattenCommandComponents().toList().executeCommand(sender)
}
suspend fun CommandSender.execute(command: Command, args: CommandArgs): Boolean = with(command) {
checkArgs(args)
return onCommand(this@execute, args)
}
suspend fun Command.execute(sender: CommandSender, args: CommandArgs): Boolean = sender.execute(this, args)
suspend fun Command.execute(sender: CommandSender, vararg args: Any): Boolean = sender.execute(this, args)
suspend fun CommandSender.execute(command: Command, vararg args: Any): Boolean = command.execute(this, args)
internal suspend fun List<Any>.executeCommand(sender: CommandSender): Boolean {
val command = InternalCommandManager.matchCommand(this) ?: return false
return command.onCommand(sender, CommandArgs.parseFrom(command, sender, this.drop(command.fullName.size)))
}
internal infix fun CommandFullName.matchesBeginning(list: List<Any>): Boolean {
this.forEachIndexed { index, any ->
if (list[index] != any) return false
}
return true
}
internal object InternalCommandManager {
@JvmField
internal val registeredCommands: MutableList<Command> = mutableListOf()
@JvmField
internal val nameToCommandMap: TreeMap<CommandFullName, Command> = TreeMap(Comparator.comparingInt { it.size })
@JvmField
internal val modifyLock = ReentrantLock()
internal fun matchCommand(splitted: List<Any>): Command? {
nameToCommandMap.entries.forEach {
if (it.key matchesBeginning splitted) return it.value
}
return null
}
}

View File

@ -20,17 +20,27 @@ data class CommandParam<T : Any>(
val type: KClass<T> // exact type
) {
constructor(name: String?, type: KClass<T>, parser: CommandArgParser<T>) : this(name, type) {
this.parser = parser
this._overrideParser = parser
}
@Suppress("PropertyName")
@JvmField
internal var parser: CommandArgParser<T>? = null
internal var _overrideParser: CommandArgParser<T>? = null
/**
* 覆盖的 [CommandArgParser].
*
* 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser]
*
* @see Command.parserFor
*/
val overrideParser: CommandArgParser<T>? get() = parser
val overrideParser: CommandArgParser<T>? get() = _overrideParser
}
fun <T : Any> CommandParam<T>.parserFrom(command: Command): CommandArgParser<T>? = command.parserFor(this)
fun <T : Any> CommandParam<T>.parserFrom(descriptor: CommandDescriptor): CommandArgParser<T>? =
descriptor.parserFor(this)
fun <T : Any> CommandParam<T>.parserFrom(context: CommandParserContext): CommandArgParser<T>? = context.parserFor(this)

View File

@ -12,7 +12,7 @@
package net.mamoe.mirai.console.command
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.AbstractCommandParserContext.Node
import net.mamoe.mirai.console.command.AbstractCommandParserContext.ParserPair
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import kotlin.internal.LowPriorityInOverloadResolution
@ -21,6 +21,7 @@ import kotlin.reflect.KClass
/**
* [KClass] [CommandArgParser] 的匹配
* @see AbstractCommandParserContext
*/
interface CommandParserContext {
operator fun <T : Any> get(klass: KClass<T>): CommandArgParser<T>?
@ -44,7 +45,14 @@ interface CommandParserContext {
})
}
fun <T : Any> CommandParserContext.parserFor(param: CommandParam<T>): CommandArgParser<T>? = this[param.type]
fun <T : Any> CommandParserContext.parserFor(param: CommandParam<T>): CommandArgParser<T>? =
param.overrideParser ?: this[param.type]
fun <T : Any> CommandDescriptor.parserFor(param: CommandParam<T>): CommandArgParser<T>? =
param.overrideParser ?: this.context.parserFor(param)
fun <T : Any> Command.parserFor(param: CommandParam<T>): CommandArgParser<T>? =
param.overrideParser ?: this.descriptor.parserFor(param)
/**
* 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser.
@ -56,8 +64,8 @@ operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandP
}
@Suppress("UNCHECKED_CAST")
open class AbstractCommandParserContext(val list: List<Node<*>>) : CommandParserContext {
class Node<T : Any>(
open class AbstractCommandParserContext(val list: List<ParserPair<*>>) : CommandParserContext {
class ParserPair<T : Any>(
val klass: KClass<T>,
val parser: CommandArgParser<T>
)
@ -92,10 +100,10 @@ inline fun CommandParserContext(block: CommandParserContextBuilder.() -> Unit):
/**
* @see CommandParserContext
*/
class CommandParserContextBuilder : MutableList<Node<*>> by mutableListOf() {
class CommandParserContextBuilder : MutableList<ParserPair<*>> by mutableListOf() {
@JvmName("add")
inline infix fun <T : Any> KClass<T>.with(parser: CommandArgParser<T>): Node<*> =
Node(this, parser)
inline infix fun <T : Any> KClass<T>.with(parser: CommandArgParser<T>): ParserPair<*> =
ParserPair(this, parser)
/**
* 添加一个指令解析器
@ -104,7 +112,7 @@ class CommandParserContextBuilder : MutableList<Node<*>> by mutableListOf() {
@LowPriorityInOverloadResolution
inline infix fun <T : Any> KClass<T>.with(
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
): Node<*> = Node(this, CommandArgParser(parser))
): ParserPair<*> = ParserPair(this, CommandArgParser(parser))
/**
* 添加一个指令解析器
@ -112,7 +120,7 @@ class CommandParserContextBuilder : MutableList<Node<*>> by mutableListOf() {
@JvmSynthetic
inline infix fun <T : Any> KClass<T>.with(
crossinline parser: CommandArgParser<T>.(s: String) -> T
): Node<*> = Node(this, CommandArgParser { s: String, _: CommandSender -> parser(s) })
): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) })
}

View File

@ -11,7 +11,6 @@ package net.mamoe.mirai.console.command
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.message.data.Message
@ -58,11 +57,13 @@ abstract class AbstractCommandSender : CommandSender {
*/
object ConsoleCommandSender : AbstractCommandSender() {
override suspend fun sendMessage(messageChain: Message) {
MiraiConsole.logger("[Command]", 0, messageChain.toString())
TODO()
// MiraiConsole.logger("[Command]", 0, messageChain.toString())
}
override suspend fun sendMessage(message: String) {
MiraiConsole.logger("[Command]", 0, message)
TODO()
// MiraiConsole.logger("[Command]", 0, message)
}
override suspend fun flushMessage() {

View File

@ -9,22 +9,26 @@
package net.mamoe.mirai.console.command
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.mamoe.mirai.Bot
import net.mamoe.mirai.Bot.Companion.botInstances
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.plugins.PluginManager
import net.mamoe.mirai.console.utils.addManager
import net.mamoe.mirai.console.utils.checkManager
import net.mamoe.mirai.console.utils.managers
import net.mamoe.mirai.console.utils.removeManager
import net.mamoe.mirai.event.subscribeMessages
import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.message.GroupMessageEvent
import net.mamoe.mirai.utils.SimpleLogger
import java.util.*
suspend fun main() {
ConsoleCommandSender.execute(DefaultCommands.Test, "test")
}
internal object DefaultCommands {
object Test : ConsoleCommand(
CommandDescriptor("test") {
param<String>()
}
) {
override suspend fun onCommand(sender: CommandSender, args: CommandArgs): Boolean {
val s = args.getReified<String>()
sender.sendMessage(s)
return true
}
}
}
/*
/**
* Some defaults commands are recommend to be replaced by plugin provided commands
@ -369,3 +373,5 @@ internal object DefaultCommands {
}
}
}
*/

View File

@ -12,13 +12,12 @@
package net.mamoe.mirai.console.plugins
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.events.EventListener
import net.mamoe.mirai.console.scheduler.PluginScheduler
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SimpleLogger
import java.io.File
import java.io.InputStream
import kotlin.coroutines.CoroutineContext
@ -28,15 +27,17 @@ import kotlin.coroutines.EmptyCoroutineContext
* 所有插件的基类
*/
abstract class PluginBase
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope, CommandOwner {
final override val coroutineContext: CoroutineContext = coroutineContext + SupervisorJob()
/**
* 插件被分配的数据目录数据目录会与插件名称同名
*/
val dataFolder: File by lazy {
TODO()
/*
File(PluginManager.pluginsPath + "/" + PluginManager.lastPluginName)
.also { it.mkdir() }
.also { it.mkdir() }*/
}
/**
@ -82,13 +83,15 @@ abstract class PluginBase
* 插件的日志
*/
val logger: MiraiLogger by lazy {
TODO()
/*
SimpleLogger("Plugin $pluginName") { priority, message, e ->
val identityString = "[${pluginName}]"
MiraiConsole.logger(priority, identityString, 0, message)
if (e != null) {
MiraiConsole.logger(priority, identityString, 0, e)
}
}
}*/
}
/**
@ -98,10 +101,13 @@ abstract class PluginBase
return try {
this.javaClass.classLoader.getResourceAsStream(fileName)
} catch (e: Exception) {
TODO()
/*
PluginManager.getFileInJarByName(
this.pluginName,
fileName
)
)*/
}
}

View File

@ -14,18 +14,18 @@ package net.mamoe.mirai.console.plugins
import kotlinx.coroutines.CancellationException
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.description
import net.mamoe.mirai.console.command.unregisterAllCommands
import net.mamoe.mirai.console.encodeToString
import net.mamoe.mirai.utils.LockFreeLinkedList
import net.mamoe.mirai.utils.SimpleLogger
import java.io.File
import java.io.InputStream
import java.net.JarURLConnection
import java.net.URL
import java.util.jar.JarFile
val PluginBase.description: PluginDescription get() = PluginManager.getPluginDescription(this)
val PluginBase.description: PluginDescription get() = TODO()
object PluginManagerOld {
/**
@ -53,9 +53,9 @@ object PluginManagerOld {
*/
@JvmOverloads
fun disablePlugins(throwable: CancellationException? = null) {
CommandManager.clearPluginsCommands()
pluginsSequence.forEach {
it.disable(throwable)
pluginsSequence.forEach { plugin ->
plugin.unregisterAllCommands()
plugin.disable(throwable)
}
nameToPluginBaseMap.clear()
pluginDescriptions.clear()
@ -87,10 +87,7 @@ object PluginManagerOld {
File(it).mkdirs()
}
private val logger = SimpleLogger("Plugin Manager") { p, message, e ->
MiraiConsole.logger(p, "[Plugin Manager]", 0, message)
MiraiConsole.logger(p, "[Plugin Manager]", 0, e)
}
private val logger = MiraiConsole.newLogger("Plugin Manager")
/**
* 加载成功的插件, 名字->插件
@ -351,7 +348,7 @@ object PluginManagerOld {
plugin: PluginBase,
exception: CancellationException? = null
) {
CommandManager.clearPluginCommands(plugin)
plugin.unregisterAllCommands()
plugin.disable(exception)
nameToPluginBaseMap.remove(plugin.pluginName)
pluginDescriptions.remove(plugin.pluginName)

View File

@ -10,7 +10,6 @@
package net.mamoe.mirai.console.plugins
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.utils.SimpleLogger
import java.io.File
import java.net.URLClassLoader
@ -18,10 +17,7 @@ internal class PluginsLoader(private val parentClassLoader: ClassLoader) {
private val loggerName = "PluginsLoader"
private val pluginLoaders = linkedMapOf<String, PluginClassLoader>()
private val classesCache = mutableMapOf<String, Class<*>>()
private val logger = SimpleLogger(loggerName) { p, message, e ->
MiraiConsole.logger(p, "[${loggerName}]", 0, message)
MiraiConsole.logger(p, "[${loggerName}]", 0, e)
}
private val logger = MiraiConsole.newLogger(loggerName)
/**
* 清除所有插件加载器

View File

@ -27,7 +27,7 @@ interface MiraiConsoleFrontEnd {
*/
val pluginCenter: PluginCenter get() = CuiPluginCenter
fun loggerFor(identity: Long): MiraiLogger
fun loggerFor(identity: String?): MiraiLogger
/**
* UI 层准备接受新增的一个BOT

View File

@ -8,6 +8,7 @@
*/
package net.mamoe.mirai.console.graphical
import kotlinx.coroutines.cancel
import net.mamoe.mirai.console.MiraiConsole
import tornadofx.launch
import kotlin.concurrent.thread
@ -24,7 +25,7 @@ class MiraiConsoleGraphicalLoader {
this.coreVersion = coreVersion
this.consoleVersion = consoleVersion
Runtime.getRuntime().addShutdownHook(thread(start = false) {
MiraiConsole.stop()
MiraiConsole.cancel()
})
launch<MiraiGraphicalUI>()
}

View File

@ -23,41 +23,46 @@ pluginManagement {
rootProject.name = 'mirai-console'
def onlyBackEnd = true
include(':mirai-console')
project(':mirai-console').dir = file("backend/mirai-console")
include(':mirai-console-pure')
project(':mirai-console-pure').dir = file("frontend/mirai-console-pure")
if (!onlyBackEnd) {
include(':mirai-console-terminal')
project(':mirai-console-terminal').dir = file("frontend/mirai-console-terminal")
include(':mirai-console-pure')
project(':mirai-console-pure').dir = file("frontend/mirai-console-pure")
try{
def javaVersion = System.getProperty("java.version")
def versionPos = javaVersion.indexOf(".")
def javaVersionNum = javaVersion.substring(0, 1).toInteger()
include(':mirai-console-terminal')
project(':mirai-console-terminal').dir = file("frontend/mirai-console-terminal")
if (javaVersion.startsWith("1.")) {
javaVersionNum = javaVersion.substring(2, 3).toInteger()
} else {
if (versionPos==-1) versionPos = javaVersion.indexOf("-")
if (versionPos==-1){
println("jdk version unknown")
}else{
javaVersionNum = javaVersion.substring(0, versionPos).toInteger()
try {
def javaVersion = System.getProperty("java.version")
def versionPos = javaVersion.indexOf(".")
def javaVersionNum = javaVersion.substring(0, 1).toInteger()
if (javaVersion.startsWith("1.")) {
javaVersionNum = javaVersion.substring(2, 3).toInteger()
} else {
if (versionPos == -1) versionPos = javaVersion.indexOf("-")
if (versionPos == -1) {
println("jdk version unknown")
} else {
javaVersionNum = javaVersion.substring(0, versionPos).toInteger()
}
}
if (javaVersionNum >= 9) {
include(':mirai-console-graphical')
project(':mirai-console-graphical').dir = file("frontend/mirai-console-graphical")
} else {
println("jdk版本为 " + javaVersionNum)
println("当前使用的 JDK 版本为 ${System.getProperty("java.version")}, 请使用JDK 9以上版本引入模块 `:mirai-console-graphical`\n")
}
}
if (javaVersionNum >= 9) {
include(':mirai-console-graphical')
project(':mirai-console-graphical').dir = file("frontend/mirai-console-graphical")
} else {
println("jdk版本为 "+ javaVersionNum)
println("当前使用的 JDK 版本为 ${System.getProperty("java.version")}, 请使用JDK 9以上版本引入模块 `:mirai-console-graphical`\n")
}
}catch(Exception ignored){
println("无法确定 JDK 版本, 将不会引入 `:mirai-console-graphical`")
} catch (Exception ignored) {
println("无法确定 JDK 版本, 将不会引入 `:mirai-console-graphical`")
}
}