diff --git a/README.md b/README.md index 63922d3fd..57f99d123 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ +
+ logo
+ + + title + +---- +Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持的高效率机器人库 + +这个项目的名字来源于 +

京都动画作品《境界的彼方》栗山未来(Kuriyama Mirai)

+

CRYPTON初音未来为代表的创作与活动(Magical Mirai)

+图标以及形象由画师DazeCake绘制 +
+ + # mirai-console 高效率插件支持 QQ 机器人框架, 机器人核心来自 [mirai](https://github.com/mamoe/mirai) diff --git a/mirai-console/build.gradle.kts b/mirai-console/build.gradle.kts index 8a1eb1389..7ad3bc229 100644 --- a/mirai-console/build.gradle.kts +++ b/mirai-console/build.gradle.kts @@ -21,6 +21,9 @@ kotlin { languageSettings.useExperimentalAnnotation("kotlin.OptIn") languageSettings.progressiveMode = true languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") } } } diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/CommandArgParser.kt b/mirai-console/src/main/java/net/mamoe/mirai/console/utils/CommandArgParser.kt deleted file mode 100644 index 6365f68b0..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/CommandArgParser.kt +++ /dev/null @@ -1,100 +0,0 @@ -@file:Suppress("NOTHING_TO_INLINE") - -package net.mamoe.mirai.console.utils - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.console.command.GroupContactCommandSender -import net.mamoe.mirai.contact.Group - -/** - * this output type of that arg - * input is always String - */ -abstract class CommandArgParser { - abstract fun parse(s: String, sender: CommandSender): T - protected inline fun parseError(message: String, cause: Throwable? = null): Nothing { - throw ParserException(message, cause) - } -} - -@Suppress("FunctionName") -inline fun CommandArgParser( - crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T -): CommandArgParser { - return object : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): T = parser(s, sender) - } -} - -/** - * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范. - */ -class ParserException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) - -inline fun Int.Companion.parser(): CommandArgParser = IntArgParser -inline fun Long.Companion.parser(): CommandArgParser = LongArgParser -inline fun Byte.Companion.parser(): CommandArgParser = ByteArgParser -inline fun Short.Companion.parser(): CommandArgParser = ShortArgParser -inline fun Float.Companion.parser(): CommandArgParser = FloatArgParser -inline fun Double.Companion.parser(): CommandArgParser = DoubleArgParser - - -object IntArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Int = s.toIntOrNull() ?: parseError("无法解析 $s 为整数") -} - -object LongArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Long = s.toLongOrNull() ?: parseError("无法解析 $s 为长整数") -} - -object ShortArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Short = s.toShortOrNull() ?: parseError("无法解析 $s 为短整数") -} - -object ByteArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Byte = s.toByteOrNull() ?: parseError("无法解析 $s 为字节") -} - -object DoubleArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Double = - s.toDoubleOrNull() ?: parseError("无法解析 $s 为小数") -} - -object FloatArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Float = - s.toFloatOrNull() ?: parseError("无法解析 $s 为小数") -} - -object StringArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): String = s -} - -/** - * require a bot that already login in console - * input: Bot UIN - * output: Bot - * errors: String->Int convert, Bot Not Exist - */ -object ExistBotArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Bot { - val uin = s.toLongOrNull() ?: parseError("无法识别机器人账号 $s") - return try { - Bot.getInstance(uin) - } catch (e: NoSuchElementException) { - error("无法找到 Bot $uin") - } - } -} - - -object ExistGroupArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Group { - if ((s == "" || s == "~") && sender is GroupContactCommandSender) { - return sender.contact as Group - } - - val code = s.toLongOrNull() ?: parseError("无法识别群号码 $s") - TODO() - } -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArg.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArg.kt deleted file mode 100644 index 6c1d4163a..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArg.kt +++ /dev/null @@ -1,272 +0,0 @@ -package net.mamoe.mirai.console.command - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.utils.fuzzySearchMember -import net.mamoe.mirai.contact.* -import net.mamoe.mirai.message.data.At -import net.mamoe.mirai.message.data.SingleMessage -import net.mamoe.mirai.message.data.content -import java.lang.NumberFormatException - -/** - * this output type of that arg - * input is always String - */ -interface CommandArg{ - operator fun invoke(s:String, commandSender: CommandSender):T = parse(s,commandSender) - - operator fun invoke(s:SingleMessage, commandSender: CommandSender):T = parse(s,commandSender) - - fun parse(s:String, commandSender: CommandSender):T - - fun parse(s:SingleMessage, commandSender: CommandSender):T -} - - -abstract class CommandArgImpl(): CommandArg { - override fun parse(s: SingleMessage, commandSender: CommandSender): T = parse(s.content,commandSender) -} - -class IntArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Int { - return try{ - s.toInt() - }catch (e:Exception){ - error("无法识别整数$s") - } - } -} - -class LongArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Long { - return try{ - s.toLong() - }catch (e:Exception){ - error("无法识别长整数$s") - } - } -} - -class DoubleArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Double { - return try{ - s.toDouble() - }catch (e:Exception){ - error("无法识别小数$s") - } - } -} - -class FloatArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Float{ - return try{ - s.toFloat() - }catch (e:Exception){ - error("无法识别小数$s") - } - } -} - -class BooleanArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Boolean { - return s.equals("true",true) || s.equals("yes",true) - } -} - -class StringArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): String { - return s - } -} - -/** - * require a bot that already login in console - * input: Bot UIN - * output: Bot - * errors: String->Int convert, Bot Not Exist - */ -class ExistBotArg : CommandArgImpl() { - override fun parse(s: String, commandSender: CommandSender): Bot { - val uin = try { - s.toLong() - } catch (e: Exception) { - error("无法识别QQ UIN$s") - } - return try { - Bot.getInstance(uin) - } catch (e: NoSuchElementException) { - error("无法找到Bot $uin") - } - } -} - - -class ExistFriendArg: CommandArgImpl(){ - //Bot.friend - //friend - //~ = self - override fun parse(s: String, commandSender: CommandSender): Friend { - if(s == "~"){ - if(commandSender !is BotAware){ - error("无法解析~作为默认") - } - val targetID = when (commandSender) { - is GroupContactCommandSender -> commandSender.realSender.id - is ContactCommandSender -> commandSender.contact.id - else -> error("无法解析~作为默认") - } - return try{ - commandSender.bot.friends[targetID] - }catch (e:NoSuchElementException){ - error("无法解析~作为默认") - } - } - if(commandSender is BotAware){ - return try{ - commandSender.bot.friends[s.toLong()] - }catch (e:NoSuchElementException){ - error("无法找到" + s + "这个好友") - }catch (e:NumberFormatException){ - error("无法解析$s") - } - }else{ - with(s.split(".")){ - if(this.size != 2){ - error("无法解析$s, 格式应为Bot.Friend") - } - return try{ - Bot.getInstance(this[0].toLong()).friends[this[1].toLong()] - }catch (e:NoSuchElementException){ - error("无法找到好友或Bot") - }catch (e:NumberFormatException){ - error("无法解析$s") - } - } - } - } - - override fun parse(s: SingleMessage, commandSender: CommandSender): Friend { - return if(s is At){ - assert(commandSender is GroupContactCommandSender) - return try { - (commandSender as BotAware).bot.friends[s.target] - }catch (e:NoSuchElementException){ - error("At的对象非Bot好友") - } - }else{ - error("无法识别Member" + s.content) - } - } -} - -class ExistGroupArg: CommandArgImpl(){ - override fun parse(s: String, commandSender: CommandSender): Group { - //by default - if ((s == "" || s == "~") && commandSender is GroupContactCommandSender) { - return commandSender.contact as Group - } - //from bot to group - if (commandSender is BotAware) { - val code = try { - s.toLong() - } catch (e: NoSuchElementException) { - error("无法识别Group Code$s") - } - return try { - commandSender.bot.getGroup(code) - } catch (e: NoSuchElementException) { - error("无法找到Group " + code + " from Bot " + commandSender.bot.id) - } - } - //from console/other - return with(s.split(".")) { - if (this.size != 2) { - error("请使用BotQQ号.群号 来表示Bot的一个群") - } - try { - Bot.getInstance(this[0].toLong()).getGroup(this[1].toLong()) - }catch (e:NoSuchElementException){ - error("无法找到" + this[0] + "的" + this[1] + "群") - }catch (e:NumberFormatException){ - error("无法识别群号或机器人UIN") - } - } - } -} - -class ExistMemberArg: CommandArgImpl(){ - //后台: Bot.Group.Member[QQ/名片] - //私聊: Group.Member[QQ/名片] - //群内: Q号 - //群内: 名片 - override fun parse(s: String, commandSender: CommandSender): Member { - if(commandSender !is BotAware){ - with(s.split(".")){ - if(this.size < 3){ - error("无法识别Member, 请使用Bot.Group.Member[QQ/名片]的格式") - } - val bot = try { - Bot.getInstance(this[0].toLong()) - }catch (e:NoSuchElementException){ - error("无法找到Bot") - }catch (e:NumberFormatException){ - error("无法识别Bot") - } - val group = try{ - bot.getGroup(this[1].toLong()) - }catch (e:NoSuchElementException){ - error("无法找到Group") - }catch (e:NumberFormatException){ - error("无法识别Group") - } - - val memberIndex = this.subList(2,this.size).joinToString(".") - return try{ - group.members[memberIndex.toLong()] - }catch (ignored:Exception){ - group.fuzzySearchMember(memberIndex)?: error("无法找到成员$memberIndex") - } - } - }else { - val bot = commandSender.bot - if(commandSender is GroupContactCommandSender){ - val group = commandSender.contact as Group - return try { - group.members[s.toLong()] - } catch (ignored: Exception) { - group.fuzzySearchMember(s) ?: error("无法找到成员$s") - } - }else { - with(s.split(".")) { - if (this.size < 2) { - error("无法识别Member, 请使用Group.Member[QQ/名片]的格式") - } - val group = try { - bot.getGroup(this[0].toLong()) - } catch (e: NoSuchElementException) { - error("无法找到Group") - } catch (e: NumberFormatException) { - error("无法识别Group") - } - - val memberIndex = this.subList(1, this.size).joinToString(".") - return try { - group.members[memberIndex.toLong()] - } catch (ignored: Exception) { - group.fuzzySearchMember(memberIndex) ?: error("无法找到成员$memberIndex") - } - } - } - } - } - - override fun parse(s: SingleMessage, commandSender: CommandSender): Member { - return if(s is At){ - assert(commandSender is GroupContactCommandSender) - ((commandSender as GroupContactCommandSender).contact as Group).members[s.target] - }else{ - error("无法识别Member" + s.content) - } - } -} - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt new file mode 100644 index 000000000..28d45513d --- /dev/null +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt @@ -0,0 +1,286 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.utils.fuzzySearchMember +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member +import net.mamoe.mirai.message.data.At +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.content +import kotlin.contracts.contract + +/** + * this output type of that arg + * input is always String + */ +abstract class CommandArgParser { + abstract fun parse(s: String, sender: CommandSender): T + open fun parse(s: SingleMessage, sender: CommandSender): T = parse(s.content, sender) +} + +@Suppress("unused") +@JvmSynthetic +inline fun CommandArgParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { + throw ParserException(message, cause) +} + +@JvmSynthetic +inline fun CommandArgParser<*>.checkArgument( + condition: Boolean, + crossinline message: () -> String = { "Check failed." } +) { + contract { + returns() implies condition + } + if (!condition) illegalArgument(message()) +} + +/** + * 创建匿名 [CommandArgParser] + */ +@Suppress("FunctionName") +@JvmSynthetic +inline fun CommandArgParser( + crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T +): CommandArgParser = object : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): T = parser(s, sender) +} + + +/** + * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范. + */ +class ParserException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) + + +object IntArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Int = + s.toIntOrNull() ?: illegalArgument("无法解析 $s 为整数") +} + +object LongArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Long = + s.toLongOrNull() ?: illegalArgument("无法解析 $s 为长整数") +} + +object ShortArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Short = + s.toShortOrNull() ?: illegalArgument("无法解析 $s 为短整数") +} + +object ByteArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Byte = + s.toByteOrNull() ?: illegalArgument("无法解析 $s 为字节") +} + +object DoubleArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Double = + s.toDoubleOrNull() ?: illegalArgument("无法解析 $s 为小数") +} + +object FloatArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Float = + s.toFloatOrNull() ?: illegalArgument("无法解析 $s 为小数") +} + +object StringArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): String = s +} + +object BooleanArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Boolean = s.trim().let { str -> + str.equals("true", ignoreCase = true) + || str.equals("yes", ignoreCase = true) + || str.equals("enabled", ignoreCase = true) + } +} + +/** + * require a bot that already login in console + * input: Bot UIN + * output: Bot + * errors: String->Int convert, Bot Not Exist + */ +object ExistBotArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Bot { + val uin = try { + s.toLong() + } catch (e: Exception) { + error("无法识别QQ UIN$s") + } + return try { + Bot.getInstance(uin) + } catch (e: NoSuchElementException) { + error("无法找到Bot $uin") + } + } +} + +object ExistFriendArgParser : CommandArgParser() { + //Bot.friend + //friend + //~ = self + override fun parse(s: String, sender: CommandSender): Friend { + if (s == "~") { + if (sender !is BotAware) { + illegalArgument("无法解析~作为默认") + } + val targetID = when (sender) { + is GroupContactCommandSender -> sender.realSender.id + is ContactCommandSender -> sender.contact.id + else -> illegalArgument("无法解析~作为默认") + } + return try { + sender.bot.friends[targetID] + } catch (e: NoSuchElementException) { + illegalArgument("无法解析~作为默认") + } + } + if (sender is BotAware) { + return try { + sender.bot.friends[s.toLong()] + } catch (e: NoSuchElementException) { + error("无法找到" + s + "这个好友") + } catch (e: NumberFormatException) { + error("无法解析$s") + } + } else { + s.split(".").let { args -> + if (args.size != 2) { + illegalArgument("无法解析 $s, 格式应为 机器人账号.好友账号") + } + return try { + Bot.getInstance(args[0].toLong()).friends.getOrNull( + args[1].toLongOrNull() ?: illegalArgument("无法解析 $s 为好友") + ) ?: illegalArgument("无法找到好友 ${args[1]}") + } catch (e: NoSuchElementException) { + illegalArgument("无法找到机器人账号 ${args[0]}") + } + } + } + } + + override fun parse(s: SingleMessage, sender: CommandSender): Friend { + if (s is At) { + assert(sender is GroupContactCommandSender) + return (sender as BotAware).bot.friends.getOrNull(s.target) ?: illegalArgument("At的对象非Bot好友") + } else { + error("无法解析 $s 为好友") + } + } +} + +object ExistGroupArgParser : CommandArgParser() { + override fun parse(s: String, sender: CommandSender): Group { + //by default + if ((s == "" || s == "~") && sender is GroupContactCommandSender) { + return sender.contact as Group + } + //from bot to group + if (sender is BotAware) { + val code = try { + s.toLong() + } catch (e: NoSuchElementException) { + error("无法识别Group Code$s") + } + return try { + sender.bot.getGroup(code) + } catch (e: NoSuchElementException) { + error("无法找到Group " + code + " from Bot " + sender.bot.id) + } + } + //from console/other + return with(s.split(".")) { + if (this.size != 2) { + error("请使用BotQQ号.群号 来表示Bot的一个群") + } + try { + Bot.getInstance(this[0].toLong()).getGroup(this[1].toLong()) + } catch (e: NoSuchElementException) { + error("无法找到" + this[0] + "的" + this[1] + "群") + } catch (e: NumberFormatException) { + error("无法识别群号或机器人UIN") + } + } + } +} + +object ExistMemberArgParser : CommandArgParser() { + //后台: Bot.Group.Member[QQ/名片] + //私聊: Group.Member[QQ/名片] + //群内: Q号 + //群内: 名片 + override fun parse(s: String, sender: CommandSender): Member { + if (sender !is BotAware) { + with(s.split(".")) { + checkArgument(this.size >= 3) { + "无法识别Member, 请使用Bot.Group.Member[QQ/名片]的格式" + } + + val bot = try { + Bot.getInstance(this[0].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Bot") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Bot") + } + + val group = try { + bot.getGroup(this[1].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Group") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Group") + } + + val memberIndex = this.subList(2, this.size).joinToString(".") + return group.members.getOrNull(memberIndex.toLong()) + ?: group.fuzzySearchMember(memberIndex) + ?: error("无法找到成员$memberIndex") + } + } else { + val bot = sender.bot + if (sender is GroupContactCommandSender) { + val group = sender.contact as Group + return try { + group.members[s.toLong()] + } catch (ignored: Exception) { + group.fuzzySearchMember(s) ?: illegalArgument("无法找到成员$s") + } + } else { + with(s.split(".")) { + if (this.size < 2) { + illegalArgument("无法识别Member, 请使用Group.Member[QQ/名片]的格式") + } + val group = try { + bot.getGroup(this[0].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Group") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Group") + } + + val memberIndex = this.subList(1, this.size).joinToString(".") + return try { + group.members[memberIndex.toLong()] + } catch (ignored: Exception) { + group.fuzzySearchMember(memberIndex) ?: illegalArgument("无法找到成员$memberIndex") + } + } + } + } + } + + override fun parse(s: SingleMessage, sender: CommandSender): Member { + return if (s is At) { + checkArgument(sender is GroupContactCommandSender) + (sender.contact as Group).members[s.target] + } else { + illegalArgument("无法识别Member" + s.content) + } + } +} + diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt new file mode 100644 index 000000000..28c5164d9 --- /dev/null +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt @@ -0,0 +1,139 @@ +@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +import kotlin.reflect.KClass + +/** + * 指令描述. 包含名称, 权限要求, 参数解析器环境, 参数列表. + */ +class CommandDescriptor( + /** + * 包含子命令的全名. 如 "`group kick`", 其中 `kick` 为 `group` 的子命令 + */ + val fullName: String, + /** + * 指令参数解析器环境. + */ + val context: CommandParserContext, + /** + * 指令参数列表, 有顺序. + */ + val params: List>, + /** + * 指令权限 + * + * @see CommandPermission.or 要求其中一个权限 + * @see CommandPermission.and 同时要求两个权限 + */ + val permission: CommandPermission = CommandPermission.Default +) + +/** + * 构建一个 [CommandDescriptor] + */ +@Suppress("FunctionName") +inline fun CommandDescriptor( + fullName: String, + block: CommandDescriptorBuilder.() -> Unit +): CommandDescriptor = CommandDescriptorBuilder(fullName).apply(block).build() + +class CommandDescriptorBuilder( + val fullName: String +) { + @PublishedApi + internal var context: CommandParserContext = CommandParserContext.Builtins + + @PublishedApi + internal var permission: CommandPermission = CommandPermission.Default + + @PublishedApi + internal var params: MutableList> = mutableListOf() + + /** 增加指令参数解析器列表 */ + @JvmSynthetic + inline fun context(block: CommandParserContextBuilder.() -> Unit) { + this.context += CommandParserContext(block) + } + + /** 增加指令参数解析器列表 */ + @JvmSynthetic + inline fun context(context: CommandParserContext): CommandDescriptorBuilder = apply { + this.context += context + } + + /** 设置权限要求 */ + fun permission(permission: CommandPermission): CommandDescriptorBuilder = apply { + this.permission = permission + } + + /** 设置权限要求 */ + @JvmSynthetic + inline fun permission(crossinline block: CommandSender.() -> Boolean) { + this.permission = AnonymousCommandPermission(block) + } + + fun param(vararg params: CommandParam<*>): CommandDescriptorBuilder = apply { + this.params.addAll(params) + } + + @JvmSynthetic + fun param( + name: String?, + type: KClass, + overrideParser: CommandArgParser? = null + ): CommandDescriptorBuilder = apply { + this.params.add(CommandParam(name, type).apply { this.parser = overrideParser }) + } + + fun param( + name: String?, + type: Class, + overrideParser: CommandArgParser? = null + ): CommandDescriptorBuilder = + param(name, type, overrideParser) + + inline fun param( + name: String? = null, + overrideParser: CommandArgParser? = null + ): CommandDescriptorBuilder = + param(name, T::class, overrideParser) + + @JvmSynthetic + fun param(vararg pairs: Pair>): CommandDescriptorBuilder = apply { + for (pair in pairs) { + this.params.add(CommandParam(pair.first, pair.second)) + } + } + + @JvmSynthetic + fun params(block: ParamBlock.() -> Unit): CommandDescriptorBuilder = apply { + ParamBlock(params).apply(block) + } + + @JvmSynthetic + fun param(type: KClass<*>): CommandDescriptorBuilder = apply { + this.params.add(CommandParam(null, type)) + } + + fun param(type: Class<*>): CommandDescriptorBuilder = apply { + this.params.add(CommandParam(null, type.kotlin)) + } + + fun build(): CommandDescriptor = CommandDescriptor(fullName, context, params, permission) +} + +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") +inline class ParamBlock internal constructor(@PublishedApi internal val list: MutableList>) { + /** 添加一个名称为 [this], 类型为 [klass] 的参数. 返回添加成功的对象 */ + infix fun String.typed(klass: KClass): CommandParam = + CommandParam(this, klass).also { list.add(it) } + + /** 指定 [CommandParam.overrideParser] */ + infix fun CommandParam.using(parser: CommandArgParser): CommandParam = + this.apply { this.parser = parser } + + /** 覆盖 [CommandArgParser] */ + inline infix fun String.using(parser: CommandArgParser): CommandParam = + this typed T::class using parser +} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt new file mode 100644 index 000000000..edd85f319 --- /dev/null +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt @@ -0,0 +1,36 @@ +@file:Suppress("unused") + +package net.mamoe.mirai.console.command + +import kotlin.reflect.KClass + +/** + * 指令形式参数. + */ +data class CommandParam( + /** + * 参数名, 为 `null` 时即为匿名参数. + * 参数名允许重复 (尽管并不建议这样做). + * 参数名仅提供给 [CommandArgParser] 以发送更好的错误信息. + */ + val name: String?, + /** + * 参数类型. 将从 [CommandDescriptor.context] 中寻找 [CommandArgParser] 解析. + */ + val type: KClass // exact type +) { + constructor(name: String?, type: KClass, parser: CommandArgParser) : this(name, type) { + this.parser = parser + } + + @JvmField + internal var parser: CommandArgParser? = null + + + /** + * 覆盖的 [CommandArgParser]. + * + * 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser] + */ + val overrideParser: CommandArgParser? get() = parser +} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt new file mode 100644 index 000000000..502628ef4 --- /dev/null +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt @@ -0,0 +1,130 @@ +/* + * 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", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.AbstractCommandParserContext.Node +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member +import kotlin.internal.LowPriorityInOverloadResolution +import kotlin.reflect.KClass + + +/** + * [KClass] 到 [CommandArgParser] 的匹配 + */ +interface CommandParserContext { + operator fun get(klass: KClass): CommandArgParser? + + /** + * 内建的默认 [CommandArgParser] + */ + object Builtins : CommandParserContext by (CommandParserContext { + Int::class with IntArgParser + Byte::class with ByteArgParser + Short::class with ShortArgParser + Boolean::class with BooleanArgParser + String::class with StringArgParser + Long::class with LongArgParser + Double::class with DoubleArgParser + Float::class with FloatArgParser + + Member::class with ExistMemberArgParser + Group::class with ExistGroupArgParser + Bot::class with ExistBotArgParser + }) +} + +fun CommandParserContext.parserFor(param: CommandParam): CommandArgParser? = this[param.type] + +/** + * 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser. + */ +operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandParserContext { + return object : CommandParserContext { + override fun get(klass: KClass): CommandArgParser? = replacer[klass] ?: this@plus[klass] + } +} + +@Suppress("UNCHECKED_CAST") +open class AbstractCommandParserContext(val list: List>) : CommandParserContext { + class Node( + val klass: KClass, + val parser: CommandArgParser + ) + + override fun get(klass: KClass): CommandArgParser? = + this.list.firstOrNull { it.klass == klass }?.parser as CommandArgParser? +} + +/** + * 构建一个 [CommandParserContext]. + * + * ``` + * CommandParserContext { + * Int::class with IntArgParser + * Member::class with ExistMemberArgParser + * Group::class with { s: String, sender: CommandSender -> + * Bot.getInstance(s.toLong()).getGroup(s.toLong()) + * } + * Bot::class with { s: String -> + * Bot.getInstance(s.toLong()) + * } + * } + * ``` + */ +@Suppress("FunctionName") +@JvmSynthetic +inline fun CommandParserContext(block: CommandParserContextBuilder.() -> Unit): CommandParserContext { + return AbstractCommandParserContext( + CommandParserContextBuilder().apply(block).distinctByReversed { it.klass }) +} + +/** + * @see CommandParserContext + */ +class CommandParserContextBuilder : MutableList> by mutableListOf() { + @JvmName("add") + inline infix fun KClass.with(parser: CommandArgParser): Node<*> = + Node(this, parser) + + /** + * 添加一个指令解析器 + */ + @JvmSynthetic + @LowPriorityInOverloadResolution + inline infix fun KClass.with( + crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T + ): Node<*> = Node(this, CommandArgParser(parser)) + + /** + * 添加一个指令解析器 + */ + @JvmSynthetic + inline infix fun KClass.with( + crossinline parser: CommandArgParser.(s: String) -> T + ): Node<*> = Node(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }) +} + + +@PublishedApi +internal inline fun Iterable.distinctByReversed(selector: (T) -> K): List { + val set = HashSet() + val list = ArrayList() + for (i in list.indices.reversed()) { + val element = list[i] + if (set.add(element.let(selector))) { + list.add(element) + } + } + return list +} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt new file mode 100644 index 000000000..af45d41a6 --- /dev/null +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt @@ -0,0 +1,204 @@ +/* + * 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", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.utils.isManager +import net.mamoe.mirai.contact.isAdministrator +import net.mamoe.mirai.contact.isOperator +import net.mamoe.mirai.contact.isOwner + +/** + * 指令权限 + * + * @see AnonymousCommandPermission + */ +abstract class CommandPermission { + /** + * 判断 [this] 是否拥有这个指令的权限 + */ + abstract fun CommandSender.hasPermission(): Boolean + + + /** + * 满足两个权限其中一个即可使用指令 + */ // no extension for Java + infix fun or(another: CommandPermission): CommandPermission = OrCommandPermission(this, another) + + /** + * 同时拥有两个权限才能使用指令 + */ // no extension for Java + infix fun and(another: CommandPermission): CommandPermission = AndCommandPermission(this, another) + + + /** + * 任何人都可以使用这个指令 + */ + object Any : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean = true + } + + /** + * 任何人都不能使用这个指令. 指令只能通过代码在 [CommandManager] 使用 + */ + object None : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean = false + } + + /** + * 管理员或群主可以使用这个指令 + */ + class Operator( + /** + * 指定只有来自某个 [Bot] 的管理员或群主才可以使用这个指令 + */ + vararg val fromBot: Long + ) : CommandPermission() { + constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) + + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isOperator() + } + + /** + * 来自任何 [Bot] 的任何一个管理员或群主都可以使用这个指令 + */ + companion object Any : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.realSender.isOperator() + } + } + } + + /** + * 群主可以使用这个指令 + */ + class GroupOwner( + /** + * 指定只有来自某个 [Bot] 的群主才可以使用这个指令 + */ + vararg val fromBot: Long + ) : CommandPermission() { + constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) + + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isOwner() + } + + /** + * 来自任何 [Bot] 的任何一个群主都可以使用这个指令 + */ + companion object Any : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.realSender.isOwner() + } + } + } + + /** + * 管理员 (不包含群主) 可以使用这个指令 + */ + class Administrator( + /** + * 指定只有来自某个 [Bot] 的管理员 (不包含群主) 才可以使用这个指令 + */ + vararg val fromBot: Long + ) : CommandPermission() { + constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) + + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isAdministrator() + } + + /** + * 来自任何 [Bot] 的任何一个管理员 (不包含群主) 都可以使用这个指令 + */ + companion object Any : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.realSender.isAdministrator() + } + } + } + + /** + * console 管理员可以使用这个指令 + */ + class Manager( + /** + * 指定只有来自某个 [Bot] 的管理员或群主才可以使用这个指令 + */ + vararg val fromBot: Long + ) : CommandPermission() { + constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) + + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isManager + } + + /** + * 任何 [Bot] 的 manager 都可以使用这个指令 + */ + companion object Any : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this is GroupContactCommandSender && this.realSender.isManager + } + } + } + + /** + * 仅控制台能使用和这个指令 + */ + object Console : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean = false + } + + companion object { + @JvmStatic + val Default: CommandPermission = Manager or Console + } +} + +/** + * 使用 [lambda][block] 快速构造 [CommandPermission] + */ +@JvmSynthetic +@Suppress("FunctionName") +inline fun AnonymousCommandPermission(crossinline block: CommandSender.() -> Boolean): CommandPermission { + return object : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean = block() + } +} + +inline fun CommandSender.hasPermission(permission: CommandPermission): Boolean = + permission.run { this@hasPermission.hasPermission() } + + +inline fun CommandPermission.hasPermission(sender: CommandSender): Boolean = this.run { sender.hasPermission() } + + +internal class OrCommandPermission( + private val first: CommandPermission, + private val second: CommandPermission +) : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this.hasPermission(first) || this.hasPermission(second) + } +} + +internal class AndCommandPermission( + private val first: CommandPermission, + private val second: CommandPermission +) : CommandPermission() { + override fun CommandSender.hasPermission(): Boolean { + return this.hasPermission(first) || this.hasPermission(second) + } +} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt index d4c2d7410..1c01500c2 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt @@ -13,8 +13,16 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.plugins.* import net.mamoe.mirai.console.utils.BotManagers.BOT_MANAGERS +import net.mamoe.mirai.contact.User import java.io.File + +/** + * 判断此用户是否为 console 管理员 + */ +val User.isManager: Boolean + get() = this.bot.managers.contains(this.id) + @OptIn(ToBeRemoved::class) internal object BotManagers { val config = File("${MiraiConsole.path}/bot.yml").loadAsConfig() diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt index 9a0d835db..5562465be 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt @@ -69,16 +69,16 @@ internal fun Throwable.addSuppressedMirai(e: Throwable) { * XXXXXYYYYY.fuzzyCompare(XXXXXYYYZZ) = 0.8 */ -fun String.fuzzyCompare(target:String):Double{ +internal fun String.fuzzyCompare(target: String): Double { var step = 0 - if(this == target){ + if (this == target) { return 1.0 } - if(target.length > this.length){ + if (target.length > this.length) { return 0.0 } - for(i in this.indices){ - if(target.length == i){ + for (i in this.indices) { + if (target.length == i) { step-- }else { if (this[i] != target[i]) { @@ -97,14 +97,14 @@ fun String.fuzzyCompare(target:String):Double{ /** * 模糊搜索一个List中index最接近target的东西 */ -inline fun Collection.fuzzySearch( +internal inline fun Collection.fuzzySearch( target: String, index: (T) -> String -):T?{ - if(this.isEmpty()){ +): T? { + if (this.isEmpty()) { return null } - var potential:T? = null + var potential: T? = null var rate = 0.0 this.forEach { val thisIndex = index(it) @@ -126,14 +126,14 @@ inline fun Collection.fuzzySearch( * 并且确保target是唯一的 * 如搜索index为XXXXYY list中同时存在XXXXYYY XXXXYYYY 将返回null */ -inline fun Collection.fuzzySearchOnly( +internal inline fun Collection.fuzzySearchOnly( target: String, index: (T) -> String -):T?{ - if(this.isEmpty()){ +): T? { + if (this.isEmpty()) { return null } - var potential:T? = null + var potential: T? = null var rate = 0.0 var collide = 0 this.forEach { @@ -154,8 +154,8 @@ inline fun Collection.fuzzySearchOnly( } -fun Group.fuzzySearchMember(nameCardTarget:String):Member?{ - return this.members.fuzzySearchOnly(nameCardTarget){ +internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { + return this.members.fuzzySearchOnly(nameCardTarget) { it.nameCard } } \ No newline at end of file