diff --git a/backend/mirai-console/build.gradle.kts b/backend/mirai-console/build.gradle.kts index f19e05e10..7de97c454 100644 --- a/backend/mirai-console/build.gradle.kts +++ b/backend/mirai-console/build.gradle.kts @@ -6,7 +6,6 @@ import java.time.Instant plugins { kotlin("jvm") kotlin("plugin.serialization") - kotlin("kapt") id("java") `maven-publish` id("com.jfrog.bintray") @@ -81,9 +80,9 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") - val autoService = "1.0-rc7" - kapt("com.google.auto.service", "auto-service", autoService) - compileOnly("com.google.auto.service", "auto-service-annotations", autoService) +// val autoService = "1.0-rc7" +// kapt("com.google.auto.service", "auto-service", autoService) +// compileOnly("com.google.auto.service", "auto-service-annotations", autoService) } ext.apply { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt index b4549263e..8f1a28139 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt @@ -9,27 +9,30 @@ package net.mamoe.mirai.console.command -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.Bot import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register +import net.mamoe.mirai.console.command.description.PermissibleIdentifierArgumentParser +import net.mamoe.mirai.console.command.description.PermissionIdArgumentParser +import net.mamoe.mirai.console.command.description.buildCommandArgumentContext import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.CommandManagerImpl.allRegisteredCommands import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip -import net.mamoe.mirai.console.util.BotManager.INSTANCE.addManager -import net.mamoe.mirai.console.util.BotManager.INSTANCE.managers -import net.mamoe.mirai.console.util.BotManager.INSTANCE.removeManager +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissibleIdentifier +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.permission.PermissionService.Companion.denyPermission +import net.mamoe.mirai.console.permission.PermissionService.Companion.getGrantedPermissions +import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.console.util.ConsoleInternalAPI import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.events.EventCancelledException -import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.message.nextMessageOrNull import net.mamoe.mirai.utils.secondsToMillis import kotlin.concurrent.thread @@ -60,33 +63,6 @@ public object BuiltInCommands { } } - public object Managers : CompositeCommand( - ConsoleCommandOwner, "managers", - description = "Manage the managers for each bot", - permission = CommandPermission.Console or CommandPermission.Manager - ), BuiltInCommand { - @Permission(CommandPermission.Console::class) - @SubCommand - public suspend fun CommandSender.add(target: User) { - target.bot.addManager(target.id) - sendMessage("已成功添加 ${target.render()} 为 ${target.bot.render()} 的管理员") - } - - @Permission(CommandPermission.Console::class) - @SubCommand - public suspend fun CommandSender.remove(target: User) { - target.bot.removeManager(target.id) - sendMessage("已成功取消 ${target.render()} 对 ${target.bot.render()} 的管理员权限") - } - - @SubCommand - public suspend fun CommandSender.list(bot: Bot) { - sendMessage("$bot 的管理员列表:\n" + bot.managers.joinToString("\n") { - bot.getFriendOrNull(it)?.render() ?: it.toString() - }) - } - } - public object Help : SimpleCommand( ConsoleCommandOwner, "help", description = "Command list" @@ -119,14 +95,15 @@ public object BuiltInCommands { closingLock.withLock { sendMessage("Stopping mirai-console") kotlin.runCatching { - MiraiConsole.job.cancelAndJoin() + ignoreException { MiraiConsole.job.cancelAndJoin() } }.fold( onSuccess = { ignoreException { sendMessage("mirai-console stopped successfully.") } }, onFailure = { + if (it is CancellationException) return@fold @OptIn(ConsoleInternalAPI::class) - MiraiConsole.mainLogger.error(it) + MiraiConsole.mainLogger.error("Exception in stop", it) ignoreException { sendMessage( it.localizedMessage ?: it.message ?: it.toString() @@ -141,7 +118,7 @@ public object BuiltInCommands { } public object Login : SimpleCommand( - ConsoleCommandOwner, "login", + ConsoleCommandOwner, "login", "登录", description = "Log in a bot account." ), BuiltInCommand { @Handler @@ -166,6 +143,40 @@ public object BuiltInCommands { ) } } + + @OptIn(ExperimentalPermission::class) + public object Permission : CompositeCommand( + ConsoleCommandOwner, "permission", "权限", + description = "Manage permissions", + overrideContext = buildCommandArgumentContext { + PermissibleIdentifier::class with PermissibleIdentifierArgumentParser + PermissionId::class with PermissionIdArgumentParser + } + ), BuiltInCommand { + // TODO: 2020/9/10 improve Permission command + @SubCommand + public suspend fun CommandSender.grant(target: PermissibleIdentifier, permission: PermissionId) { + target.grantPermission(permission) + sendMessage("OK") + } + + @SubCommand + public suspend fun CommandSender.deny(target: PermissibleIdentifier, permission: PermissionId) { + target.denyPermission(permission) + sendMessage("OK") + } + + @SubCommand("grantedPermissions", "gp") + public suspend fun CommandSender.grantedPermissions(target: PermissibleIdentifier) { + val grantedPermissions = target.getGrantedPermissions() + sendMessage(grantedPermissions.joinToString("\n") { it.id.toString() }) + } + + @SubCommand("listPermissions", "lp") + public suspend fun CommandSender.listPermissions() { + sendMessage(PermissionService.INSTANCE.getRegisteredPermissions().joinToString("\n") { it.id.toString() }) + } + } } internal inline fun ignoreException(block: () -> R): R? { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index 2dcca4945..cf757ee70 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -15,7 +15,11 @@ import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.java.JCommand +import net.mamoe.mirai.console.internal.command.createCommandPermission import net.mamoe.mirai.console.internal.command.isValidSubName +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.SingleMessage @@ -51,7 +55,8 @@ public interface Command { /** * 指令权限 */ - public val permission: CommandPermission + @ExperimentalPermission + public val permission: Permission /** * 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 @@ -95,13 +100,14 @@ public suspend inline fun Command.onCommand(sender: CommandSender, args: Message * @see CompositeCommand * @see RawCommand */ -public abstract class AbstractCommand @JvmOverloads constructor( +public abstract class AbstractCommand +@OptIn(ExperimentalPermission::class) +@JvmOverloads constructor( /** 指令拥有者. */ public override val owner: CommandOwner, vararg names: String, description: String = "", - /** 指令权限 */ - public override val permission: CommandPermission = CommandPermission.Default, + parentPermission: PermissionId = owner.basePermission, /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ public override val prefixOptional: Boolean = false ) : Command { @@ -111,4 +117,6 @@ public abstract class AbstractCommand @JvmOverloads constructor( list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") } }.toTypedArray() + @OptIn(ExperimentalPermission::class) + public override val permission: Permission by lazy { createCommandPermission(parentPermission) } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt index 49d2715f3..a8682841d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandOwner.kt @@ -10,6 +10,10 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionIdNamespace +import net.mamoe.mirai.console.permission.RootPermission import net.mamoe.mirai.console.plugin.jvm.JvmPlugin /** @@ -20,9 +24,25 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPlugin * * @see JvmPlugin 是一个 [CommandOwner] */ -public interface CommandOwner +@OptIn(ExperimentalPermission::class) +public interface CommandOwner : PermissionIdNamespace { + /** + * 此 [PermissionIdNamespace] 拥有的指令都默认将 [basePermission] 作为父权限. + * + * TODO document + */ + @ExperimentalPermission + public val basePermission: PermissionId +} /** * 代表控制台所有者. 所有的 mirai-console 内建的指令都属于 [ConsoleCommandOwner]. */ -internal object ConsoleCommandOwner : CommandOwner \ No newline at end of file +internal object ConsoleCommandOwner : CommandOwner { + @ExperimentalPermission + override val basePermission: PermissionId + get() = RootPermission.id + + @ExperimentalPermission + override fun permissionId(id: String): PermissionId = PermissionId("console", id) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt deleted file mode 100644 index 6ea195f7d..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("unused", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.command - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand -import net.mamoe.mirai.console.internal.command.AndCommandPermissionImpl -import net.mamoe.mirai.console.internal.command.OrCommandPermissionImpl -import net.mamoe.mirai.console.util.BotManager.INSTANCE.isManager -import net.mamoe.mirai.contact.isAdministrator -import net.mamoe.mirai.contact.isOperator -import net.mamoe.mirai.contact.isOwner - -/** - * 指令权限. - * - * 在 [CommandManager.executeCommand] 时将会检查权限. - * - * @see Command.permission 从指令获取权限 - */ -public fun interface CommandPermission { - /** - * 判断 [this] 是否拥有这个指令的权限 - * - * @see CommandSender.hasPermission - * @see CommandPermission.testPermission - */ - public fun CommandSender.hasPermission(): Boolean - - - /** - * 满足两个权限其中一个即可使用指令 - */ // no extension for Java - public infix fun or(another: CommandPermission): CommandPermission = OrCommandPermissionImpl(this, another) - - /** - * 同时拥有两个权限才能使用指令 - */ // no extension for Java - public infix fun and(another: CommandPermission): CommandPermission = AndCommandPermissionImpl(this, another) - - - /** - * 任何人都可以使用这个指令 - */ - public object Any : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean = true - } - - /** - * 任何人都不能使用这个指令. 指令只能通过调用 [Command.onCommand] 执行. - */ - public object None : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean = false - } - - /** - * 来自任何 [Bot] 的任何一个管理员或群主都可以使用这个指令 - */ - public object Operator : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean { - return this is MemberCommandSender && this.user.isOperator() - } - } - - /** - * 来自任何 [Bot] 的任何一个群主都可以使用这个指令 - */ - public object GroupOwner : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean { - return this is MemberCommandSender && this.user.isOwner() - } - } - - /** - * 管理员 (不包含群主) 可以使用这个指令 - */ - public object GroupAdmin : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean { - return this is MemberCommandSender && this.user.isAdministrator() - } - } - - /** - * 任何 [Bot] 的 manager 都可以使用这个指令 - */ - public object Manager : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean { - return this is MemberCommandSender && this.user.isManager - } - } - - /** - * 仅控制台能使用和这个指令 - */ - public object Console : CommandPermission { - public override fun CommandSender.hasPermission(): Boolean = this is ConsoleCommandSender - } - - /** - * 默认权限. - * - * @return [Manager] or [Console] - */ - public object Default : CommandPermission by (Manager or Console) -} - -/** - * 判断 [this] 是否拥有权限 [permission] - * - * @see CommandSender.hasPermission - * @see CommandPermission.testPermission - * @see CommandPermission.hasPermission - */ -public inline fun CommandSender.hasPermission(permission: CommandPermission): Boolean = - permission.run { this@hasPermission.hasPermission() } - - -/** - * 判断 [sender] 是否拥有权限 [this] - * - * @see CommandSender.hasPermission - * @see CommandPermission.testPermission - * @see CommandPermission.hasPermission - */ -public inline fun CommandPermission.testPermission(sender: CommandSender): Boolean = this.run { sender.hasPermission() } - -/** - * 判断 [sender] 是否拥有权限 [Command.permission] - * - * @see CommandSender.hasPermission - * @see CommandPermission.testPermission - * @see CommandPermission.hasPermission - */ -public inline fun Command.testPermission(sender: CommandSender): Boolean = sender.hasPermission(this.permission) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt index 1dbe2a424..af199bcb4 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -32,6 +32,10 @@ import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.internal.data.castOrNull import net.mamoe.mirai.console.internal.plugin.rootCauseOrSelf +import net.mamoe.mirai.console.permission.AbstractPermissibleIdentifier +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permissible +import net.mamoe.mirai.console.permission.PermissibleIdentifier import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScopeContext @@ -132,7 +136,8 @@ import kotlin.internal.LowPriorityInOverloadResolution * @see toCommandSender * @see asCommandSender */ -public interface CommandSender : CoroutineScope { +@OptIn(ExperimentalPermission::class) +public interface CommandSender : CoroutineScope, Permissible { /** * 与这个 [CommandSender] 相关的 [Bot]. * 当通过控制台执行时为 `null`. @@ -509,6 +514,9 @@ public abstract class ConsoleCommandSender @ConsoleFrontEndImplementation constr public final override val name: String get() = NAME public final override fun toString(): String = NAME + @ExperimentalPermission + public final override val identifier: PermissibleIdentifier = AbstractPermissibleIdentifier.Console + public companion object INSTANCE : ConsoleCommandSender(), CoroutineScope { public const val NAME: String = "ConsoleCommandSender" public override val coroutineContext: CoroutineContext by lazy { MiraiConsole.childScopeContext(NAME) } @@ -604,6 +612,9 @@ public open class FriendCommandSender internal constructor( public override val subject: Contact get() = user public override fun toString(): String = "FriendCommandSender($user)" + @ExperimentalPermission + public override val identifier: PermissibleIdentifier = AbstractPermissibleIdentifier.ExactFriend(user.id) + @JvmBlockingBridge public override suspend fun sendMessage(message: String): MessageReceipt = sendMessage(PlainText(message)) @@ -620,10 +631,13 @@ public open class MemberCommandSender internal constructor( ) : AbstractUserCommandSender(), GroupAwareCommandSender, CoroutineScope by user.childScope("MemberCommandSender") { - public override val group: Group get() = user.group + public final override val group: Group get() = user.group public override val subject: Group get() = group public override fun toString(): String = "MemberCommandSender($user)" + @ExperimentalPermission + public override val identifier: PermissibleIdentifier = AbstractPermissibleIdentifier.ExactMember(group.id, user.id) + @JvmBlockingBridge public override suspend fun sendMessage(message: String): MessageReceipt = sendMessage(PlainText(message)) @@ -644,6 +658,10 @@ public open class TempCommandSender internal constructor( public override val subject: Contact get() = group public override fun toString(): String = "TempCommandSender($user)" + @ExperimentalPermission + public override val identifier: PermissibleIdentifier = + AbstractPermissibleIdentifier.ExactTemp(user.group.id, user.id) + @JvmBlockingBridge public override suspend fun sendMessage(message: String): MessageReceipt = sendMessage(PlainText(message)) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt index c901ea4ca..02c8fc910 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -20,11 +20,12 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.message.data.MessageChain import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION -import kotlin.reflect.KClass /** * 复合指令. 指令注册时候会通过反射构造指令解析器. @@ -81,14 +82,14 @@ import kotlin.reflect.KClass * @see buildCommandArgumentContext */ @ConsoleExperimentalAPI -public abstract class CompositeCommand( +public abstract class CompositeCommand @OptIn(ExperimentalPermission::class) constructor( owner: CommandOwner, vararg names: String, description: String = "no description available", - permission: CommandPermission = CommandPermission.Default, + parentPermission: PermissionId = owner.basePermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext -) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), +) : Command, AbstractReflectionCommand(owner, names, description, parentPermission, prefixOptional), CommandArgumentContextAware { /** @@ -112,7 +113,8 @@ public abstract class CompositeCommand( /** 指定子指令要求的权限 */ @Retention(RUNTIME) @Target(FUNCTION) - protected annotation class Permission(val value: KClass) + @ExperimentalPermission + protected annotation class Permission(val value: String) /** 指令描述 */ @Retention(RUNTIME) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt index 57a436a4f..47ac57558 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -14,6 +14,10 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.java.JRawCommand +import net.mamoe.mirai.console.internal.command.createCommandPermission +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.message.data.MessageChain /** @@ -27,7 +31,7 @@ import net.mamoe.mirai.message.data.MessageChain * @see SimpleCommand 简单指令 * @see CompositeCommand 复合指令 */ -public abstract class RawCommand( +public abstract class RawCommand @OptIn(ExperimentalPermission::class) constructor( /** * 指令拥有者. * @see CommandOwner @@ -39,11 +43,14 @@ public abstract class RawCommand( public override val usage: String = "", /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ public override val description: String = "", - /** 指令权限 */ - public override val permission: CommandPermission = CommandPermission.Default, + /** 指令父权限 */ + parentPermission: PermissionId = owner.basePermission, /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ public override val prefixOptional: Boolean = false ) : Command { + @OptIn(ExperimentalPermission::class) + public override val permission: Permission by lazy { createCommandPermission(parentPermission) } + /** * 在指令被执行时调用. * diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt index 12c4a16a2..29cd1e791 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -22,6 +22,8 @@ import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.command.java.JSimpleCommand import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.message.data.MessageChain /** @@ -47,14 +49,14 @@ import net.mamoe.mirai.message.data.MessageChain * @see JSimpleCommand Java 实现 * @see [CommandManager.executeCommand] */ -public abstract class SimpleCommand( +public abstract class SimpleCommand @OptIn(ExperimentalPermission::class) constructor( owner: CommandOwner, vararg names: String, description: String = "no description available", - permission: CommandPermission = CommandPermission.Default, + basePermission: PermissionId = owner.basePermission, prefixOptional: Boolean = false, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext -) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), +) : Command, AbstractReflectionCommand(owner, names, description, basePermission, prefixOptional), CommandArgumentContextAware { /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt index a72d00f9d..e43b9a190 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgumentParserBuiltins.kt @@ -11,7 +11,12 @@ package net.mamoe.mirai.console.command.description import net.mamoe.mirai.Bot import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.CommandSender.Companion.asCommandSender import net.mamoe.mirai.console.internal.command.fuzzySearchMember +import net.mamoe.mirai.console.permission.AbstractPermissibleIdentifier +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissibleIdentifier +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.contact.* import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getGroupOrNull @@ -303,6 +308,31 @@ public object ExistingMemberArgumentParser : InternalCommandArgumentParserExtens } } +@ExperimentalPermission +public object PermissionIdArgumentParser : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): PermissionId { + return kotlin.runCatching { PermissionId.parseFromString(raw) }.getOrElse { + illegalArgument("无法解析 $raw 为 PermissionId") + } + } +} + +@ExperimentalPermission +public object PermissibleIdentifierArgumentParser : CommandArgumentParser { + override fun parse(raw: String, sender: CommandSender): PermissibleIdentifier { + return kotlin.runCatching { AbstractPermissibleIdentifier.parseFromString(raw) }.getOrElse { + illegalArgument("无法解析 $raw 为 PermissionId") + } + } + + override fun parse(raw: MessageContent, sender: CommandSender): PermissibleIdentifier { + if (raw is At) { + return ExistingUserArgumentParser.parse(raw, sender).asCommandSender(false).identifier + } + return super.parse(raw, sender) + } +} + internal interface InternalCommandArgumentParserExtensions : CommandArgumentParser { fun String.parseToLongOrFail(): Long = toLongOrNull() ?: illegalArgument("无法解析 $this 为整数") diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt index ee585a8cc..2e190d600 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JCompositeCommand.kt @@ -9,8 +9,13 @@ package net.mamoe.mirai.console.command.java -import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.BuiltInCommands +import net.mamoe.mirai.console.command.CommandManager +import net.mamoe.mirai.console.command.CommandOwner +import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.description.buildCommandArgumentContext +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.util.ConsoleExperimentalAPI /** @@ -64,16 +69,18 @@ import net.mamoe.mirai.console.util.ConsoleExperimentalAPI * @see buildCommandArgumentContext */ @ConsoleExperimentalAPI -public abstract class JCompositeCommand( +public abstract class JCompositeCommand @OptIn(ExperimentalPermission::class) +@JvmOverloads constructor( owner: CommandOwner, - vararg names: String -) : CompositeCommand(owner, *names) { + vararg names: String, + parentPermission: PermissionId = owner.basePermission, +) : CompositeCommand(owner, *names, parentPermission = parentPermission) { /** 指令描述, 用于显示在 [BuiltInCommands.Help] */ public final override var description: String = "" protected set - /** 指令权限 */ - public final override var permission: CommandPermission = CommandPermission.Default + @OptIn(ExperimentalPermission::class) + public final override var permission: net.mamoe.mirai.console.permission.Permission = super.permission protected set /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt index d3b3fb87a..9074cc170 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JRawCommand.kt @@ -13,6 +13,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute +import net.mamoe.mirai.console.internal.command.createCommandPermission +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.SingleMessage @@ -42,14 +46,16 @@ import net.mamoe.mirai.message.data.SingleMessage * * @see JRawCommand */ -public abstract class JRawCommand( +public abstract class JRawCommand @OptIn(ExperimentalPermission::class) +@JvmOverloads constructor( /** * 指令拥有者. * @see CommandOwner */ public override val owner: CommandOwner, /** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ - public override vararg val names: String + public override vararg val names: String, + parentPermission: PermissionId = owner.basePermission, ) : Command { /** 用法说明, 用于发送给用户 */ public override var usage: String = "" @@ -60,7 +66,8 @@ public abstract class JRawCommand( protected set /** 指令权限 */ - public final override var permission: CommandPermission = CommandPermission.Default + @ExperimentalPermission + public final override var permission: Permission = createCommandPermission(parentPermission) protected set /** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */ diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt index 25d886a55..8a830713b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/java/JSimpleCommand.kt @@ -12,9 +12,11 @@ package net.mamoe.mirai.console.command.java import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandOwner -import net.mamoe.mirai.console.command.CommandPermission import net.mamoe.mirai.console.command.SimpleCommand import net.mamoe.mirai.console.command.description.CommandArgumentContext +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId /** * Java 实现: @@ -39,13 +41,16 @@ import net.mamoe.mirai.console.command.description.CommandArgumentContext * @see SimpleCommand * @see [CommandManager.executeCommand] */ -public abstract class JSimpleCommand( +public abstract class JSimpleCommand @OptIn(ExperimentalPermission::class) constructor( owner: CommandOwner, - vararg names: String -) : SimpleCommand(owner, *names) { + vararg names: String, + basePermission: PermissionId, +) : SimpleCommand(owner, *names, basePermission = basePermission) { public override var description: String = super.description protected set - public override var permission: CommandPermission = super.permission + + @ExperimentalPermission + public override var permission: Permission = super.permission protected set public override var prefixOptional: Boolean = super.prefixOptional protected set diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt index 8523353bd..3a610c6f5 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataExtensions.kt @@ -1,15 +1,58 @@ -@file:Suppress("unused", "INAPPLICABLE_JVM_NAME") +@file:Suppress("unused", "INAPPLICABLE_JVM_NAME", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") package net.mamoe.mirai.console.data import net.mamoe.mirai.console.data.PluginDataExtensions.withDefault import net.mamoe.mirai.console.internal.data.ShadowMap +import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +import kotlin.internal.LowPriorityInOverloadResolution /** * [PluginData] 相关一些扩展 */ public object PluginDataExtensions { + @ConsoleExperimentalAPI + public open class NotNullMap internal constructor( + private val delegate: Map + ) : Map by delegate { + override fun get(key: K): V = + delegate[key] ?: error("Internal error: delegate[key] returned null for NotNullMap.get") + + @Deprecated( + "getOrDefault on NotNullMap always returns the value in the map, and defaultValue will never be returned.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this.get(key)") + ) + override fun getOrDefault(key: K, defaultValue: V): V { + return super.getOrDefault(key, defaultValue) + } + } + + @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") // as designed + public class NotNullMutableMap internal constructor( + private val delegate: MutableMap + ) : MutableMap by delegate, NotNullMap(delegate) { + override fun get(key: K): V = + delegate[key] ?: error("Internal error: delegate[key] returned null for NotNullMutableMap.get") + + @Deprecated( + "getOrDefault on NotNullMutableMap always returns the value in the map, and defaultValue will never be returned.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this.get(key)") + ) + override fun getOrDefault(key: K, defaultValue: V): V { + return super.getOrDefault(key, defaultValue) + } + + @Deprecated( + "putIfAbsent on NotNullMutableMap always does nothing.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("") + ) + override fun putIfAbsent(key: K, value: V): Nothing? = null + } + /** * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先放入一个 [LinkedHashMap], 再从 [this] 中取出链接自动保存的 [LinkedHashMap]. ([MutableMap.getOrPut] 的替代) * @@ -17,7 +60,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultMapImmutable") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { LinkedHashMap() } } @@ -27,7 +70,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultMap") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { LinkedHashMap() } } @@ -38,7 +81,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultListImmutable") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { ArrayList() } } @@ -48,7 +91,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultList") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { ArrayList() } } @@ -59,7 +102,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultSetImmutable") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { LinkedHashSet() } } @@ -69,7 +112,7 @@ public object PluginDataExtensions { */ @JvmName("withEmptyDefaultSet") @JvmStatic - public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { + public fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> { return this.withDefault { LinkedHashSet() } } @@ -78,15 +121,47 @@ public object PluginDataExtensions { * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先调用 [defaultValueComputer] 并放入 [Map], 再返回调用的返回值 */ @JvmStatic + @JvmName("withDefaultMapImmutableNotNull") + public fun SerializerAwareValue>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue> { + @Suppress("UNCHECKED_CAST") // magic + return (this as SerializerAwareValue>).withDefault(defaultValueComputer) as SerializerAwareValue> + } + + /** + * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先调用 [defaultValueComputer] 并放入 [Map], 再返回调用的返回值 + */ + @JvmStatic + @LowPriorityInOverloadResolution @JvmName("withDefaultMapImmutable") public fun SerializerAwareValue>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue> { @Suppress("UNCHECKED_CAST") // magic return (this as SerializerAwareValue>).withDefault(defaultValueComputer) as SerializerAwareValue> } + @JvmStatic + @JvmName("withDefaultMapNotNull") + public fun SerializerAwareValue>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue> { + val origin = this + + @Suppress("UNCHECKED_CAST") + return SerializableValue( + object : CompositeMapValue { + private val instance = NotNullMutableMap(createDelegateInstance(origin, defaultValueComputer)) + + override var value: Map + get() = instance + set(value) { + origin.value = value as MutableMap // erased cast + } + } as Value>, // erased cast + this.serializer + ) + } + /** * 创建一个代理对象, 当 [Map.get] 返回 `null` 时先调用 [defaultValueComputer] 并放入 [Map], 再返回调用的返回值 */ + @LowPriorityInOverloadResolution @JvmStatic @JvmName("withDefaultMap") public fun SerializerAwareValue>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue> { @@ -95,24 +170,7 @@ public object PluginDataExtensions { @Suppress("UNCHECKED_CAST") return SerializableValue( object : CompositeMapValue { - private val instance = object : MutableMap, AbstractMap() { - override val entries: MutableSet> get() = origin.value.entries - override val keys: MutableSet get() = origin.value.keys - override val values: MutableCollection get() = origin.value.values - override fun clear() = origin.value.clear() - override fun putAll(from: Map) = origin.value.putAll(from) - override fun remove(key: K): V? = origin.value.remove(key) - override fun put(key: K, value: V): V? = origin.value.put(key, value) - - override fun get(key: K): V? { - // the only difference - val result = origin.value[key] - if (result != null) return result - put(key, defaultValueComputer(key)) - return origin.value[key] - } - } - + private val instance = createDelegateInstance(origin, defaultValueComputer) override var value: Map get() = instance set(value) { @@ -123,6 +181,57 @@ public object PluginDataExtensions { ) } + private fun createDelegateInstance( + origin: SerializerAwareValue>, + defaultValueComputer: (K) -> V, + ): MutableMap { + return object : MutableMap, AbstractMap() { + override val entries: MutableSet> get() = origin.value.entries + override val keys: MutableSet get() = origin.value.keys + override val values: MutableCollection get() = origin.value.values + override fun clear() = origin.value.clear() + override fun putAll(from: Map) = origin.value.putAll(from) + override fun remove(key: K): V? = origin.value.remove(key) + override fun put(key: K, value: V): V? = origin.value.put(key, value) + + override fun get(key: K): V? { + // the only difference + val result = origin.value[key] + if (result != null) return result + put(key, defaultValueComputer(key)) + return origin.value[key] + } + } + } + + + /** + * 替换 [MutableMap] 的 key + */ + @JvmName("mapKeysNotNull") + @JvmStatic + public fun SerializerAwareValue>.mapKeys( + oldToNew: (OldK) -> NewK, + newToOld: (NewK) -> OldK, + ): SerializerAwareValue> { + val origin = this + + @Suppress("UNCHECKED_CAST") + return SerializableValue( + object : CompositeMapValue { + private val instance = + NotNullMutableMap(ShadowMap({ origin.value }, oldToNew, newToOld, { it }, { it })) + + override var value: Map + get() = instance + set(value) { + origin.value = + value.mapKeysTo(NotNullMutableMap(LinkedHashMap())) { it.key.let(newToOld) } // erased cast + } + } as Value>, // erased cast + this.serializer + ) + } /** * 替换 [MutableMap] 的 key @@ -177,4 +286,72 @@ public object PluginDataExtensions { this.serializer ) } -} \ No newline at end of file + + /** + * 替换 [Map] 的 key + */ + @JvmName("mapKeysImmutableNotNull") + @JvmStatic + public fun SerializerAwareValue>.mapKeys( + oldToNew: (OldK) -> NewK, + newToOld: (NewK) -> OldK, + ): SerializerAwareValue> { + val origin = this + + @Suppress("UNCHECKED_CAST") + return SerializableValue( + object : CompositeMapValue { + // casting Map to MutableMap is OK here, as we don't call mutable functions + private val instance = + NotNullMap(ShadowMap({ origin.value as MutableMap }, oldToNew, newToOld, { it }, { it })) + + override var value: Map + get() = instance + set(value) { + origin.value = + value.mapKeysTo(NotNullMutableMap(LinkedHashMap())) { it.key.let(newToOld) } // erased cast + } + } as Value>, // erased cast + this.serializer + ) + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/Value.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/Value.kt index c5ccbcd3b..95ff1850a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/Value.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/Value.kt @@ -58,6 +58,26 @@ public class SerializableValue( ) : Value by delegate, SerializerAwareValue { public override fun toString(): String = delegate.toString() + public override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other?.javaClass != this.javaClass) return false + + @Suppress("UNCHECKED_CAST") + other as SerializableValue + if (other.delegate != this.delegate) return false + // if (other.serializer != this.serializer) return false + // TODO: 2020/9/9 serializers should be checked here, but it will cause incomparable issue when putting a SerializableValue as a Key + return true + } + + override fun hashCode(): Int { + @Suppress("UnnecessaryVariable", "CanBeVal") + var result = delegate.hashCode() + // result = 31 * result + serializer.hashCode() + // TODO: 2020/9/9 serializers should be checked here, but it will cause incomparable issue when putting a SerializableValue as a Key + return result + } + public companion object { @JvmStatic @JvmName("create") diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/Extension.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/Extension.kt index 0c0250174..50930174b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/Extension.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/Extension.kt @@ -13,9 +13,15 @@ import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.extensions.PluginLoaderProvider import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +/** + * 表示一个扩展. + */ @ConsoleExperimentalAPI public interface Extension +/** + * 增加一些函数 (方法)的扩展 + */ @ConsoleExperimentalAPI public interface FunctionExtension : Extension diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/AbstractExtensionPoint.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt similarity index 64% rename from backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/AbstractExtensionPoint.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt index 4a52ef01a..ebb30b3ec 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/AbstractExtensionPoint.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionPoint.kt @@ -11,39 +11,77 @@ package net.mamoe.mirai.console.extension +import net.mamoe.mirai.console.extensions.SingletonExtensionSelector import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginLoader import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +import java.util.* import java.util.concurrent.CopyOnWriteArraySet import kotlin.contracts.contract import kotlin.internal.LowPriorityInOverloadResolution import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf +@ConsoleExperimentalAPI +public interface ExtensionPoint { + public val type: KClass + + public fun registerExtension(plugin: Plugin, extension: T) + public fun getExtensions(): Set> + + public companion object { + @JvmStatic + @JvmSynthetic + @ConsoleExperimentalAPI + public inline fun ExtensionPoint<*>.isFor(exactType: Boolean = false): Boolean { + return if (exactType) { + T::class == type + } else T::class.isSubclassOf(type) + } + } +} + +@ConsoleExperimentalAPI +public interface SingletonExtensionPoint> : ExtensionPoint { + public companion object { + @JvmStatic + @ConsoleExperimentalAPI + public fun > SingletonExtensionPoint.findSingleton(): T? { + return SingletonExtensionSelector.selectSingleton(type, this.getExtensions()) + } + } +} + +/** + * 表示一个扩展点 + */ @ConsoleExperimentalAPI public open class AbstractExtensionPoint( @ConsoleExperimentalAPI - public val type: KClass -) { - - @ConsoleExperimentalAPI - public data class ExtensionRegistry( - public val plugin: Plugin, - public val extension: T - ) + public override val type: KClass +) : ExtensionPoint { + init { + @Suppress("LeakingThis") + allExtensionPoints.add(this) + } private val instances: MutableSet> = CopyOnWriteArraySet() - @Synchronized @ConsoleExperimentalAPI - public fun registerExtension(plugin: Plugin, extension: T) { - require(plugin.isEnabled) { "Plugin $plugin must be enabled before registering an extension." } + public override fun registerExtension(plugin: Plugin, extension: T) { + // require(plugin.isEnabled) { "Plugin $plugin must be enabled before registering an extension." } + requireNotNull(extension::class.qualifiedName) { "Extension must not be an anonymous object" } instances.add(ExtensionRegistry(plugin, extension)) } - @Synchronized - internal fun getExtensions(): Set> = instances + public override fun getExtensions(): Set> = Collections.unmodifiableSet(instances) + + internal companion object { + @ConsoleExperimentalAPI + internal val allExtensionPoints: MutableList> = mutableListOf() + } } @@ -105,7 +143,7 @@ internal fun AbstractExtensionPoint.throwExtensionExcepti throwable: Throwable ) { throw ExtensionException( - "Exception while executing extension ${extension.kClassQualifiedNameOrTip} from ${plugin.name}, registered for ${this.type.qualifiedName}", + "Exception while executing extension ${extension.kClassQualifiedNameOrTip} provided by plugin '${plugin.name}', registered for ${this.type.qualifiedName}", throwable ) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionRegistry.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionRegistry.kt new file mode 100644 index 000000000..d79b1cdf3 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extension/ExtensionRegistry.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.extension + +import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.util.ConsoleExperimentalAPI + +@ConsoleExperimentalAPI +public data class ExtensionRegistry( + public val plugin: Plugin, + public val extension: T +) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt index 69eda41c4..fd1c7c0dd 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/BotConfigurationAlterer.kt @@ -5,7 +5,6 @@ package net.mamoe.mirai.console.extensions import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.FunctionExtension -import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.utils.BotConfiguration /** @@ -13,7 +12,6 @@ import net.mamoe.mirai.utils.BotConfiguration * * @see MiraiConsole.addBot */ -@ConsoleExperimentalAPI public interface BotConfigurationAlterer : FunctionExtension { /** diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt index 2d35375d8..7dfaa5e39 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PermissionServiceProvider.kt @@ -2,11 +2,19 @@ package net.mamoe.mirai.console.extensions import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.SingletonExtension +import net.mamoe.mirai.console.extension.SingletonExtensionPoint +import net.mamoe.mirai.console.permission.ExperimentalPermission import net.mamoe.mirai.console.permission.PermissionService -import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +import net.mamoe.mirai.console.plugin.description.PluginKind -@ConsoleExperimentalAPI -public interface PermissionServiceProvider : SingletonExtension { +/** + * [权限服务][PermissionService] 提供器. + * + * 此扩展可由 [PluginKind.LOADER] 和 [PluginKind.HIGH_PRIORITY_EXTENSIONS] 插件提供 + */ +@ExperimentalPermission +public interface PermissionServiceProvider : SingletonExtension> { public companion object ExtensionPoint : - AbstractExtensionPoint(PermissionServiceProvider::class) + AbstractExtensionPoint(PermissionServiceProvider::class), + SingletonExtensionPoint } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt index cf2271981..5b5016de9 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PluginLoaderProvider.kt @@ -3,9 +3,12 @@ package net.mamoe.mirai.console.extensions import net.mamoe.mirai.console.extension.AbstractExtensionPoint import net.mamoe.mirai.console.extension.InstanceExtension import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.description.PluginKind /** * 提供扩展 [PluginLoader] + * + * 此扩展可由 [PluginKind.LOADER] 插件提供 */ public interface PluginLoaderProvider : InstanceExtension> { public companion object ExtensionPoint : AbstractExtensionPoint(PluginLoaderProvider::class) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt new file mode 100644 index 000000000..aca64ca2f --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/PostStartupExtension.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.extensions + +import net.mamoe.mirai.console.extension.AbstractExtensionPoint +import net.mamoe.mirai.console.extension.FunctionExtension + +/** + * 在 Console 启动完成后立即在主线程调用的扩展. 用于进行一些必要的延迟初始化. + * + * 这些扩展只会, 且一定会被调用正好一次. + * + * 此扩展可由所有插件提供 + */ +public fun interface PostStartupExtension : FunctionExtension { + /** + * 将在 Console 主线程执行. + */ + public operator fun invoke() + + public companion object ExtensionPoint : AbstractExtensionPoint(PostStartupExtension::class) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt new file mode 100644 index 000000000..fce60e204 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/extensions/SingletonExtensionSelector.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.extensions + +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.extension.* +import net.mamoe.mirai.console.internal.extensions.BuiltInSingletonExtensionSelector +import net.mamoe.mirai.console.plugin.description.PluginKind +import net.mamoe.mirai.console.plugin.name +import net.mamoe.mirai.utils.info +import kotlin.reflect.KClass + +/** + * 用于同时拥有多个 [SingletonExtension] 时选择一个实例. + * + * 如有多个 [SingletonExtensionSelector] 注册, 将会停止服务器. + * + * 此扩展可由 [PluginKind.LOADER] 和 [PluginKind.HIGH_PRIORITY_EXTENSIONS] 插件提供 + */ +public interface SingletonExtensionSelector : FunctionExtension { + + public fun selectSingleton( + extensionType: KClass, + candidates: Collection> + ): T? + + public companion object ExtensionPoint : + AbstractExtensionPoint(SingletonExtensionSelector::class) { + internal val instance: SingletonExtensionSelector by lazy { + val instances = SingletonExtensionSelector.getExtensions() + when { + instances.isEmpty() -> BuiltInSingletonExtensionSelector + instances.size == 1 -> { + instances.single().also { (plugin, ext) -> + MiraiConsole.mainLogger.info { "Loaded SingletonExtensionSelector: $ext from ${plugin.name}" } + }.extension + } + else -> { + error("Found too many SingletonExtensionSelectors: ${instances.joinToString { (p, i) -> "'$i' from '${p.name}'" }}. Check your plugins and ensure there is only one external SingletonExtensionSelectors") + } + } + } + + internal fun selectSingleton( + extensionType: KClass, + candidates: Collection> + ): T? = + instance.selectSingleton(extensionType, candidates) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt index b1a292caa..c40a49b7e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt @@ -15,7 +15,9 @@ import com.vdurmont.semver4j.Semver import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking import net.mamoe.mirai.Bot +import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.console.MalformedMiraiConsoleImplementationError import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription @@ -23,12 +25,20 @@ import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.CommandManager +import net.mamoe.mirai.console.command.ConsoleCommandSender import net.mamoe.mirai.console.data.PluginDataStorage +import net.mamoe.mirai.console.extension.useExtensions +import net.mamoe.mirai.console.extensions.PostStartupExtension +import net.mamoe.mirai.console.extensions.SingletonExtensionSelector import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig import net.mamoe.mirai.console.internal.data.builtins.ConsoleDataScope -import net.mamoe.mirai.console.internal.plugin.CuiPluginCenter import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl +import net.mamoe.mirai.console.permission.BuiltInPermissionService +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.permission.PermissionService.Companion.grantPermission +import net.mamoe.mirai.console.permission.RootPermission import net.mamoe.mirai.console.plugin.PluginLoader import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.center.PluginCenter @@ -39,6 +49,8 @@ import java.nio.file.Path import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext /** @@ -46,7 +58,7 @@ import kotlin.coroutines.CoroutineContext */ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleImplementation, MiraiConsole { - override val pluginCenter: PluginCenter get() = CuiPluginCenter + override val pluginCenter: PluginCenter get() = throw UnsupportedOperationException("PluginCenter is not supported yet") private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate @@ -76,49 +88,116 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI override fun createLogger(identity: String?): MiraiLogger = instance.createLogger(identity) - @OptIn(ConsoleExperimentalAPI::class) + @OptIn(ConsoleExperimentalAPI::class, ExperimentalPermission::class) + @Suppress("RemoveRedundantBackticks") internal fun doStart() { - val buildDateFormatted = - buildDate.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - mainLogger.info { "Starting mirai-console..." } - mainLogger.info { "Backend: version $version, built on $buildDateFormatted." } - mainLogger.info { frontEndDescription.render() } + phase `greeting`@{ + val buildDateFormatted = + buildDate.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - if (coroutineContext[Job] == null) { - throw MalformedMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.") - } - if (coroutineContext[CoroutineExceptionHandler] == null) { - throw MalformedMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a CoroutineExceptionHandler in it.") + mainLogger.info { "Starting mirai-console..." } + mainLogger.info { "Backend: version $version, built on $buildDateFormatted." } + mainLogger.info { frontEndDescription.render() } } - MiraiConsole.job.invokeOnCompletion { - Bot.botInstances.forEach { kotlin.runCatching { it.close() }.exceptionOrNull()?.let(mainLogger::error) } + phase `check coroutineContext`@{ + if (coroutineContext[Job] == null) { + throw MalformedMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.") + } + if (coroutineContext[CoroutineExceptionHandler] == null) { + throw MalformedMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a CoroutineExceptionHandler in it.") + } + + MiraiConsole.job.invokeOnCompletion { + Bot.botInstances.forEach { kotlin.runCatching { it.close() }.exceptionOrNull()?.let(mainLogger::error) } + } } - mainLogger.info { "Reloading configurations..." } - ConsoleDataScope.reloadAll() + ConsoleInput - BuiltInCommands.registerAll() - mainLogger.info { "Prepared built-in commands: ${BuiltInCommands.all.joinToString { it.primaryName }}" } - CommandManager - CommandManagerImpl.commandListener // start + // start + + phase `load configurations`@{ + mainLogger.verbose { "Loading configurations..." } + ConsoleDataScope.reloadAll() + } + + val pluginLoadSession: PluginManagerImpl.PluginLoadSession + + phase `load plugins`@{ + PluginManager // init + + mainLogger.verbose { "Loading PluginLoader provider plugins..." } + PluginManagerImpl.loadEnablePluginProviderPlugins() + mainLogger.verbose { "${PluginManager.plugins.size} such plugin(s) loaded." } + + mainLogger.verbose { "Scanning high-priority extension and normal plugins..." } + pluginLoadSession = PluginManagerImpl.scanPluginsUsingPluginLoadersIncludingThoseFromPluginLoaderProvider() + mainLogger.verbose { "${pluginLoadSession.allKindsOfPlugins.size} plugin(s) found." } + + mainLogger.verbose { "Loading Extension provider plugins..." } + PluginManagerImpl.loadEnableHighPriorityExtensionPlugins(pluginLoadSession) + mainLogger.verbose { "${PluginManager.plugins.size} such plugin(s) loaded." } + } + + SingletonExtensionSelector.instance // init + + phase `load PermissionService`@{ + mainLogger.verbose { "Loading PermissionService..." } + PermissionService.INSTANCE.let { ps -> + if (ps is BuiltInPermissionService) { + ConsoleDataScope.addAndReloadConfig(ps.config) + mainLogger.verbose { "Reloaded PermissionService settings." } + } + } + + ConsoleCommandSender.grantPermission(RootPermission) + } + + phase `prepare commands`@{ + mainLogger.verbose { "Loading built-in commands..." } + BuiltInCommands.registerAll() + mainLogger.verbose { "Prepared built-in commands: ${BuiltInCommands.all.joinToString { it.primaryName }}" } + CommandManager + CommandManagerImpl.commandListener // start + } + + phase `load normal plugins`@{ + mainLogger.verbose { "Loading normal plugins..." } + val count = PluginManagerImpl.loadEnableNormalPlugins(pluginLoadSession) + mainLogger.verbose { "$count normal plugin(s) loaded." } + } + + mainLogger.info { "${PluginManagerImpl.plugins.size} plugin(s) loaded." } + + phase `auto-login bots`@{ + runBlocking { + for ((id, password) in AutoLoginConfig.plainPasswords) { + mainLogger.info { "Auto-login $id" } + MiraiConsole.addBot(id, password).alsoLogin() + } + + for ((id, password) in AutoLoginConfig.md5Passwords) { + mainLogger.info { "Auto-login $id" } + MiraiConsole.addBot(id, password).alsoLogin() + } + } + } + + PostStartupExtension.useExtensions { it() } - mainLogger.info { "Loading plugins..." } - PluginManager - PluginManagerImpl.loadEnablePlugins() - mainLogger.info { "${PluginManager.plugins.size} plugin(s) loaded." } mainLogger.info { "mirai-console started successfully." } + } - for ((id, password) in AutoLoginConfig.plainPasswords) { - mainLogger.info { "Auto-login $id" } - MiraiConsole.addBot(id, password) + @Retention(AnnotationRetention.SOURCE) + @DslMarker + private annotation class MiraiIsCool + + @MiraiIsCool + private inline fun phase(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - - for ((id, password) in AutoLoginConfig.md5Passwords) { - mainLogger.info { "Auto-login $id" } - MiraiConsole.addBot(id, password) - } - - // Only for initialize + block() } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt index 7fde28a03..02e6c3312 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandManagerImpl.kt @@ -66,7 +66,7 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine when (val result = sender.executeCommand(message)) { is CommandExecuteResult.PermissionDenied -> { - if (!result.command.prefixOptional) { + if (!result.command.prefixOptional || message.content.startsWith(CommandManager.commandPrefix)) { sender.sendMessage("权限不足") intercept() } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandPermissionImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandPermissionImpl.kt deleted file mode 100644 index 8c98125fb..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandPermissionImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.console.internal.command - -import net.mamoe.mirai.console.command.CommandPermission -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.console.command.hasPermission - -internal class OrCommandPermissionImpl( - private val first: CommandPermission, - private val second: CommandPermission -) : CommandPermission { - override fun CommandSender.hasPermission(): Boolean { - return this.hasPermission(first) || this.hasPermission(second) - } -} - -internal class AndCommandPermissionImpl( - 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/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt index e0c5b78b5..b427d982e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CompositeCommandInternal.kt @@ -16,11 +16,19 @@ import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentContextAware import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission import net.mamoe.mirai.message.data.* import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.KFunction -import kotlin.reflect.full.* +import kotlin.reflect.full.callSuspend +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.isSubclassOf internal object CompositeCommandSubCommandAnnotationResolver : AbstractReflectionCommand.SubCommandAnnotationResolver { @@ -40,17 +48,18 @@ internal object SimpleCommandSubCommandAnnotationResolver : baseCommand.names } -internal abstract class AbstractReflectionCommand @JvmOverloads constructor( +internal abstract class AbstractReflectionCommand @OptIn(ExperimentalPermission::class) +@JvmOverloads constructor( owner: CommandOwner, names: Array, description: String = "", - permission: CommandPermission = CommandPermission.Default, + parentPermission: PermissionId = owner.basePermission, prefixOptional: Boolean = false ) : Command, AbstractCommand( owner, names = names, description = description, - permission = permission, + parentPermission = parentPermission, prefixOptional = prefixOptional ), CommandArgumentContextAware { internal abstract val subCommandAnnotationResolver: SubCommandAnnotationResolver @@ -70,7 +79,7 @@ internal abstract class AbstractReflectionCommand @JvmOverloads constructor( internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { DefaultSubCommandDescriptor( "", - permission, + createCommandPermission(parentPermission), onCommand = { sender: CommandSender, args: MessageChain -> sender.onDefault(args) } @@ -113,21 +122,23 @@ internal abstract class AbstractReflectionCommand @JvmOverloads constructor( } } - internal class DefaultSubCommandDescriptor( + internal class DefaultSubCommandDescriptor @OptIn(ExperimentalPermission::class) constructor( val description: String, - val permission: CommandPermission, + val permission: Permission, val onCommand: suspend (sender: CommandSender, rawArgs: MessageChain) -> Unit ) - internal inner class SubCommandDescriptor( + internal inner class SubCommandDescriptor @OptIn(ExperimentalPermission::class) constructor( val names: Array, val params: Array>, val description: String, - val permission: CommandPermission, + val permission: Permission, val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean, val context: CommandArgumentContext ) { val usage: String = createUsage(this@AbstractReflectionCommand) + + @OptIn(ExperimentalPermission::class) internal suspend fun parseAndExecute( sender: CommandSender, argsWithSubCommandNameNotRemoved: MessageChain, @@ -209,10 +220,6 @@ internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain { internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = findAnnotation() != null -internal inline fun KClass.getInstance(): T { - return this.objectInstance ?: this.createInstance() -} - internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "" internal fun Array.createUsage(baseCommand: AbstractReflectionCommand): String = @@ -227,7 +234,7 @@ internal fun Array.createUsage(b internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseCommand: AbstractReflectionCommand): String = buildString { - if (!baseCommand.prefixOptional) { + if (baseCommand.prefixOptional) { append("(") append(CommandManager.commandPrefix) append(")") @@ -246,6 +253,7 @@ internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseComm appendLine() }.trimEnd() +@OptIn(ExperimentalPermission::class) internal fun AbstractReflectionCommand.createSubCommand( function: KFunction<*>, context: CommandArgumentContext @@ -322,8 +330,8 @@ internal fun AbstractReflectionCommand.createSubCommand( return SubCommandDescriptor( commandName, params, - subDescription, - overridePermission?.value?.getInstance() ?: permission, + subDescription, // overridePermission?.value + overridePermission?.value?.let { PermissionService.INSTANCE[PermissionId.parseFromString(it)] } ?: permission, onCommand = { sender: CommandSender, args: Array -> val result = if (notStatic) { if (hasSenderParam) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt index cd8ef90a9..1958f7c61 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/internal.kt @@ -10,6 +10,12 @@ package net.mamoe.mirai.console.internal.command import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.Command.Companion.primaryName +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.Permission +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.permission.PermissionService.Companion.testPermission import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member import net.mamoe.mirai.message.data.MessageChain @@ -136,12 +142,14 @@ internal fun Group.fuzzySearchMember( } } +@OptIn(ExperimentalPermission::class) +internal fun Command.createCommandPermission(parent: PermissionId): Permission { + return PermissionService.INSTANCE.register(owner.permissionId(primaryName), description, parent) +} //// internal -@JvmSynthetic -internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } - +@OptIn(ExperimentalPermission::class) @JvmSynthetic @Throws(CommandExecutionException::class) internal suspend fun CommandSender.executeCommandInternal( @@ -150,7 +158,7 @@ internal suspend fun CommandSender.executeCommandInternal( commandName: String, checkPermission: Boolean ): CommandExecuteResult { - if (checkPermission && !command.testPermission(this)) { + if (checkPermission && !command.permission.testPermission(this)) { return CommandExecuteResult.PermissionDenied(command, commandName) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt index aae4ef01b..bfb93c4c5 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/MultiFilePluginDataStorageImpl.kt @@ -14,6 +14,7 @@ import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.PluginDataHolder import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip +import net.mamoe.mirai.console.permission.ExperimentalPermission import net.mamoe.mirai.console.util.ConsoleExperimentalAPI import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.SilentLogger @@ -54,7 +55,7 @@ internal open class MultiFilePluginDataStorageImpl( } dir.mkdir() - val file = dir.resolve(name) + val file = dir.resolve("$name.yml") if (file.isDirectory) { error("Target File $file is occupied by a directory therefore data ${instance::class.qualifiedNameOrTip} can't be saved.") } @@ -64,7 +65,26 @@ internal open class MultiFilePluginDataStorageImpl( @ConsoleExperimentalAPI public override fun store(holder: PluginDataHolder, instance: PluginData) { - getPluginDataFile(holder, instance).writeText(Yaml.default.encodeToString(instance.updaterSerializer, Unit)) + @OptIn(ExperimentalPermission::class) + val yaml =/* if (instance.saveName == "PermissionService") Json { + prettyPrint = true + ignoreUnknownKeys = true + isLenient = true + allowStructuredMapKeys = true + } /*Yaml( + configuration = YamlConfiguration( + mapSerialization = YamlConfiguration.MapSerialization.FLOW_MAP, + listSerialization = YamlConfiguration.ListSerialization.FLOW_SEQUENCE, + classSerialization = YamlConfiguration.MapSerialization.FLOW_MAP + ) + )*/ else */Yaml.default + getPluginDataFile(holder, instance).writeText( + kotlin.runCatching { + yaml.encodeToString(instance.updaterSerializer, Unit) + }.getOrElse { + throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it) + } + ) logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.valueNodes.size} properties)" } } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/_PluginData.value.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/_PluginData.value.kt index 8dad18e32..7e045e30e 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/_PluginData.value.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/_PluginData.value.kt @@ -196,4 +196,21 @@ internal class LazyReferenceValueImpl : ReferenceValue, AbstractValueImpl< initialied = true valueField = value } + + override fun toString(): String { + return valueField.toString() + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other?.javaClass != this.javaClass) return false + + other as LazyReferenceValueImpl<*> + if (other.valueField != valueField) return false + return true + } + + override fun hashCode(): Int { + return valueField?.hashCode() ?: 0 + } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/BotManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/BotManagerImpl.kt deleted file mode 100644 index 0cdf45731..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/BotManagerImpl.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.internal.data.builtins - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.data.AutoSavePluginConfig -import net.mamoe.mirai.console.data.PluginDataExtensions.mapKeys -import net.mamoe.mirai.console.data.PluginDataExtensions.withEmptyDefault -import net.mamoe.mirai.console.data.ValueDescription -import net.mamoe.mirai.console.data.value -import net.mamoe.mirai.console.util.BotManager -import net.mamoe.mirai.contact.User - -internal object BotManagerImpl : BotManager { - override val User.isManager: Boolean get() = this.id in ManagersConfig[this.bot] - - override fun Bot.removeManager(id: Long): Boolean { - return ManagersConfig[this].remove(id) - } - - override val Bot.managers: List - get() = ManagersConfig[this].toList() - - override fun Bot.addManager(id: Long): Boolean { - return ManagersConfig[this].add(id) - } -} - -internal object ManagersConfig : AutoSavePluginConfig() { - override val saveName: String - get() = "Managers" - - @ValueDescription( - """ - 管理员列表 - """ - ) - private val managers by value>>().withEmptyDefault() - .mapKeys(Bot::getInstance, Bot::id) - - internal operator fun get(bot: Bot): MutableSet = managers[bot]!! -} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/ConsoleDataScope.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/ConsoleDataScope.kt index 177dd533d..98b8077bd 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/ConsoleDataScope.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/builtins/ConsoleDataScope.kt @@ -21,8 +21,13 @@ import net.mamoe.mirai.utils.minutesToMillis internal object ConsoleDataScope : CoroutineScope by MiraiConsole.childScope("ConsoleDataScope") { - private val data: Array = arrayOf() - private val configs: Array = arrayOf(ManagersConfig, AutoLoginConfig) + private val data: List = mutableListOf() + private val configs: MutableList = mutableListOf(AutoLoginConfig) + + fun addAndReloadConfig(config: PluginConfig) { + configs.add(config) + ConsoleBuiltInPluginConfigStorage.load(ConsoleBuiltInPluginConfigHolder, config) + } fun reloadAll() { data.forEach { dt -> @@ -37,13 +42,13 @@ internal object ConsoleDataScope : CoroutineScope by MiraiConsole.childScope("Co internal object ConsoleBuiltInPluginDataHolder : AutoSavePluginDataHolder, CoroutineScope by ConsoleDataScope.childScope("ConsoleBuiltInPluginDataHolder") { override val autoSaveIntervalMillis: LongRange = 1.minutesToMillis..10.minutesToMillis - override val dataHolderName: String get() = "ConsoleBuiltIns" + override val dataHolderName: String get() = "Console" } internal object ConsoleBuiltInPluginConfigHolder : AutoSavePluginDataHolder, CoroutineScope by ConsoleDataScope.childScope("ConsoleBuiltInPluginConfigHolder") { override val autoSaveIntervalMillis: LongRange = 1.minutesToMillis..10.minutesToMillis - override val dataHolderName: String get() = "ConsoleBuiltIns" + override val dataHolderName: String get() = "Console" } internal object ConsoleBuiltInPluginDataStorage : diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/serializerHelper.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/serializerHelper.kt index 728b8ac00..eced14e96 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/serializerHelper.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/serializerHelper.kt @@ -47,7 +47,7 @@ internal fun serializerMirai(type: KType): KSerializer { typeArguments.isEmpty() -> rootClass.serializer() else -> { val serializers = typeArguments - .map(::serializer) + .map(::serializerMirai) // Array is not supported, see KT-32839 when (rootClass) { List::class, MutableList::class, ArrayList::class -> ListSerializer(serializers[0]) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extensions/BuiltInSingletonExtensionSelector.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extensions/BuiltInSingletonExtensionSelector.kt new file mode 100644 index 000000000..5d84aabdf --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/extensions/BuiltInSingletonExtensionSelector.kt @@ -0,0 +1,69 @@ +package net.mamoe.mirai.console.internal.extensions + +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.data.AutoSavePluginData +import net.mamoe.mirai.console.data.value +import net.mamoe.mirai.console.extension.Extension +import net.mamoe.mirai.console.extension.ExtensionRegistry +import net.mamoe.mirai.console.extensions.SingletonExtensionSelector +import net.mamoe.mirai.console.internal.data.kClassQualifiedName +import net.mamoe.mirai.console.plugin.name +import net.mamoe.mirai.console.util.ConsoleInput +import net.mamoe.mirai.utils.info +import kotlin.reflect.KClass + +internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector { + + private val config: SaveData = SaveData() + + private class SaveData : AutoSavePluginData() { + override val saveName: String get() = "ExtensionSelector" + + val value: MutableMap by value() + } + + override fun selectSingleton( + extensionType: KClass, + candidates: Collection> + ): T? = when { + candidates.isEmpty() -> null + candidates.size == 1 -> candidates.single().extension + else -> kotlin.run { + val target = config.value[extensionType.qualifiedName!!] + ?: return promptForSelectionAndSave(extensionType, candidates) + + val found = candidates.firstOrNull { it.extension::class.qualifiedName == target }?.extension + ?: return promptForSelectionAndSave(extensionType, candidates) + + found + } + } + + private fun promptForSelectionAndSave( + extensionType: KClass, + candidates: Collection> + ) = promptForManualSelection(extensionType, candidates) + .also { config.value[extensionType.qualifiedName!!] = it.extension.kClassQualifiedName!! }.extension + + private fun promptForManualSelection( + extensionType: KClass, + candidates: Collection> + ): ExtensionRegistry { + MiraiConsole.mainLogger.info { "There are multiple ${extensionType.simpleName}s, please select one:" } + + val candidatesList = candidates.toList() + + for ((index, candidate) in candidatesList.withIndex()) { + MiraiConsole.mainLogger.info { "${index + 1}. '${candidate.extension}' from '${candidate.plugin.name}'" } + } + + MiraiConsole.mainLogger.info { "Please choose a number from 1 to ${candidatesList.count()}" } + + val choiceStr = runBlocking { ConsoleInput.requestInput("Your choice: ") } + + val choice = choiceStr.toIntOrNull() ?: error("Bad choice") + + return candidatesList[choice - 1] + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/CuiPluginCenter.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/CuiPluginCenter.kt deleted file mode 100644 index 42ac52d11..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/CuiPluginCenter.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:OptIn(ConsoleExperimentalAPI::class) - -package net.mamoe.mirai.console.internal.plugin - -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.request.* -import io.ktor.util.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import net.mamoe.mirai.console.plugin.center.PluginCenter -import net.mamoe.mirai.console.util.ConsoleExperimentalAPI -import net.mamoe.mirai.console.util.retryCatching -import java.io.File - -internal val json = Json { - isLenient = true - ignoreUnknownKeys = true -} - -@OptIn(KtorExperimentalAPI::class) -internal val Http = HttpClient(CIO) - -internal object CuiPluginCenter : PluginCenter { - - var plugins: List? = null - - /** - * 一页 10 个 pageMinNum=1 - */ - override suspend fun fetchPlugin(page: Int): Map { - check(page > 0) - val startIndex = (page - 1) * 10 - val endIndex = startIndex + 9 - val map = mutableMapOf() - (startIndex until endIndex).forEach { index -> - val plugins = plugins ?: kotlin.run { - refresh() - plugins - } ?: return mapOf() - - if (index >= plugins.size) { - return@forEach - } - - map[name] = plugins[index] - } - return map - } - - override suspend fun findPlugin(name: String): PluginCenter.PluginInfo? { - val result = retryCatching(3) { - Http.get("https://miraiapi.jasonczc.cn/getPluginDetailedInfo?name=$name") - }.getOrElse { return null } - if (result == "err:not found") return null - - return json.decodeFromString(PluginCenter.PluginInfo.serializer(), result) - } - - override suspend fun refresh() { - - @Serializable - data class Result( - val success: Boolean, - val result: List - ) - - val result = json.decodeFromString(Result.serializer(), Http.get("https://miraiapi.jasonczc.cn/getPluginList")) - - check(result.success) { "Failed to fetch plugin list from Cui Cloud" } - plugins = result.result - } - - override suspend fun 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) { - tryNTimes { - val con = - URL("https://pan.jasonczc.cn/?/mirai/plugins/$name/$name-" + info.version + ".mp4").openConnection() as HttpURLConnection - val input = con.inputStream - val size = con.contentLength - var totalDownload = 0F - val outputStream = FileOutputStream(targetFile) - var len: Int - val buff = ByteArray(1024) - while (input.read(buff).also { len = it } != -1) { - totalDownload += len - outputStream.write(buff, 0, len) - progressListener.invoke(this@downloadPlugin, totalDownload / size) - } - } - } - return targetFile - */ - } - - override val name: String get() = "崔云" -} - diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JarPluginLoaderImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JarPluginLoaderImpl.kt index 829f11ea3..ffcb6e9e3 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JarPluginLoaderImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JarPluginLoaderImpl.kt @@ -22,7 +22,6 @@ import net.mamoe.mirai.console.plugin.PluginLoadException import net.mamoe.mirai.console.plugin.jvm.* import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.utils.MiraiLogger -import net.mamoe.mirai.utils.info import java.io.File import java.net.URLClassLoader import java.util.concurrent.ConcurrentHashMap @@ -74,8 +73,8 @@ internal object JarPluginLoaderImpl : JvmPluginClassLoader(arrayOf(it.toURI().toURL()), MiraiConsole::class.java.classLoader, classLoaders) }.onEach { (_, classLoader) -> classLoaders.add(classLoader) - }.asSequence().findAllInstances().onEach { loaded -> - logger.info { "Successfully initialized JvmPlugin ${loaded}." } + }.asSequence().findAllInstances().onEach { + //logger.verbose { "Successfully initialized JvmPlugin ${loaded}." } }.onEach { (file, plugin) -> pluginFileToInstanceMap[file] = plugin } + pluginFileToInstanceMap.asSequence() @@ -98,9 +97,13 @@ internal object JarPluginLoaderImpl : override fun enable(plugin: JvmPlugin) { if (plugin.isEnabled) return ensureActive() - if (plugin is JvmPluginInternal) { - plugin.internalOnEnable() - } else plugin.onEnable() + runCatching { + if (plugin is JvmPluginInternal) { + plugin.internalOnEnable() + } else plugin.onEnable() + }.getOrElse { + throw PluginLoadException("Exception while loading ${plugin.description.name}", it) + } } override fun disable(plugin: JvmPlugin) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt index 43fabd034..061bf6a15 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginInternal.kt @@ -15,11 +15,16 @@ import kotlinx.coroutines.* import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.data.runCatchingLog import net.mamoe.mirai.console.internal.data.mkdir +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionId +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.permission.PermissionService.Companion.allocatePermissionIdForPlugin import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceContainer import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.util.NamedSupervisorJob import net.mamoe.mirai.utils.MiraiLogger import java.io.File @@ -38,10 +43,24 @@ internal abstract class JvmPluginInternal( parentCoroutineContext: CoroutineContext ) : JvmPlugin, CoroutineScope { + @OptIn(ExperimentalPermission::class) + final override val basePermission: PermissionId by lazy { + PermissionService.INSTANCE.register( + PermissionService.INSTANCE.allocatePermissionIdForPlugin(name, "*"), + "The base permission" + ).id + } + final override var isEnabled: Boolean = false private val resourceContainerDelegate by lazy { this::class.java.classLoader.asResourceContainer() } - override fun getResourceAsStream(path: String): InputStream? = resourceContainerDelegate.getResourceAsStream(path) + final override fun getResourceAsStream(path: String): InputStream? = + resourceContainerDelegate.getResourceAsStream(path) + + @OptIn(ExperimentalPermission::class) + override fun permissionId(id: String): PermissionId { + return PermissionId(description.name, id) + } // region JvmPlugin final override val logger: MiraiLogger by lazy { @@ -62,11 +81,11 @@ internal abstract class JvmPluginInternal( dataFolderPath.toFile() } - override val configFolderPath: Path by lazy { + final override val configFolderPath: Path by lazy { PluginManager.pluginsConfigPath.resolve(description.name).apply { mkdir() } } - override val configFolder: File by lazy { + final override val configFolder: File by lazy { configFolderPath.toFile() } @@ -91,6 +110,7 @@ internal abstract class JvmPluginInternal( } internal fun internalOnEnable(): Boolean { + basePermission if (!firstRun) refreshCoroutineContext() kotlin.runCatching { onEnable() @@ -116,6 +136,7 @@ internal abstract class JvmPluginInternal( internal val _intrinsicCoroutineContext: CoroutineContext by lazy { CoroutineName("Plugin $dataHolderName") } + @JvmField internal val coroutineContextInitializer = { CoroutineExceptionHandler { context, throwable -> diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt index 4bec6f753..bdef81153 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt @@ -113,64 +113,84 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol ) } - /** - * STEPS: - * 1. 遍历插件列表, 使用 [builtInLoaders] 加载 [PluginKind.LOADER] 类型的插件 - * 2. [启动][PluginLoader.enable] 所有 [PluginKind.LOADER] 的插件 - * 3. 使用内建和所有插件提供的 [PluginLoader] 加载全部除 [PluginKind.LOADER] 外的插件列表. - * 4. 解决依赖并排序 - * 5. 依次 [PluginLoader.load] - * 但不 [PluginLoader.enable] - * - * @return [builtInLoaders] 可以加载的插件. 已经完成了 [PluginLoader.load], 但没有 [PluginLoader.enable] - */ - @Suppress("UNCHECKED_CAST") - @Throws(PluginMissingDependencyException::class) - internal fun loadEnablePlugins() { - loadAndEnableLoaderProviders() - loadPluginLoaderProvidedByPlugins() + internal class PluginLoadSession( + val allKindsOfPlugins: List, List>> + ) + + // Phase #2 + internal fun scanPluginsUsingPluginLoadersIncludingThoseFromPluginLoaderProvider(): PluginLoadSession { + return PluginLoadSession(loadersLock.withLock { _pluginLoaders.listAllPlugins() }) + } + + // Phase #0 + internal fun loadEnablePluginProviderPlugins() { + loadAndEnableLoaderProvidersUsingBuiltInLoaders() + } + + // Phase #3 + internal fun loadEnableHighPriorityExtensionPlugins(session: PluginLoadSession): Int { loadersLock.withLock { - _pluginLoaders.listAllPlugins().flatMap { it.second } - .also { - logger.debug("All plugins: ${it.joinToString { (_, desc, _) -> desc.name }}") - } + session.allKindsOfPlugins.flatMap { it.second } + .filter { it.kind == PluginKind.HIGH_PRIORITY_EXTENSIONS } .sortByDependencies() - .also { - logger.debug("Sorted plugins: ${it.joinToString { (_, desc, _) -> desc.name }}") - } - .loadAndEnableAllInOrder() + .also { it.loadAndEnableAllInOrder() } + .let { return it.size } } } - private fun loadPluginLoaderProvidedByPlugins() { + // Phase #4 + internal fun loadEnableNormalPlugins(session: PluginLoadSession): Int { loadersLock.withLock { - PluginLoaderProvider.useExtensions { - logger.info { "Loaded PluginLoader ${it.instance} from $" } - _pluginLoaders.add(it.instance) + session.allKindsOfPlugins.flatMap { it.second } + .filter { it.kind == PluginKind.NORMAL } + .sortByDependencies() + .also { it.loadAndEnableAllInOrder() } + .let { return it.size } + } + } + + // Phase #1 + internal fun loadPluginLoaderProvidedByPlugins() { + loadersLock.withLock { + PluginLoaderProvider.useExtensions { ext, plugin -> + logger.info { "Loaded PluginLoader ${ext.instance} from ${plugin.name}" } + _pluginLoaders.add(ext.instance) } } } private fun List.loadAndEnableAllInOrder() { - return this.forEach { (loader, _, plugin) -> + this.forEach { (loader, _, plugin) -> loader.loadPluginNoEnable(plugin) + } + this.forEach { (loader, _, plugin) -> loader.enablePlugin(plugin) } } + @kotlin.jvm.Throws(PluginLoadException::class) + internal fun checkPluginDescription(description: PluginDescription) { + kotlin.runCatching { + PluginDescription.checkPluginDescription(description) + }.getOrElse { + throw PluginLoadException("PluginDescription check failed.", it) + } + } + /** * @return [builtInLoaders] 可以加载的插件. 已经完成了 [PluginLoader.load], 但没有 [PluginLoader.enable] */ @Suppress("UNCHECKED_CAST") @Throws(PluginMissingDependencyException::class) - private fun loadAndEnableLoaderProviders(): List { + private fun loadAndEnableLoaderProvidersUsingBuiltInLoaders(): List { val allDescriptions = builtInLoaders.listAllPlugins() .asSequence() .onEach { (loader, descriptions) -> loader as PluginLoader + descriptions.forEach(PluginManagerImpl::checkPluginDescription) descriptions.filter { it.kind == PluginKind.LOADER }.sortByDependencies().loadAndEnableAllInOrder() } .flatMap { it.second.asSequence() } @@ -196,7 +216,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol return cannotBeLoad } - fun List.filterIsMissing(): List = + fun Collection.filterIsMissing(): List = this.filterNot { it.isOptional || it in resolved } tailrec fun List.doSort() { @@ -239,4 +259,4 @@ internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>, plugin: Plug ) internal operator fun List.contains(dependency: PluginDependency): Boolean = - any { it.name == dependency.name } + any { it.id == dependency.id } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/AbstractConcurrentPermissionService.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/AbstractConcurrentPermissionService.kt new file mode 100644 index 000000000..12baee7b6 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/AbstractConcurrentPermissionService.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +import net.mamoe.mirai.console.permission.PermissibleIdentifier.Companion.grantedWith +import java.util.concurrent.CopyOnWriteArrayList + +/** + * + */ +@ExperimentalPermission +public abstract class AbstractConcurrentPermissionService

: PermissionService

{ + protected abstract val permissions: MutableMap + protected abstract val grantedPermissionsMap: MutableMap> + + protected abstract fun createPermission( + id: PermissionId, + description: String, + base: PermissionId = RootPermission.id + ): P + + override fun get(id: PermissionId): P? = permissions[id] + + override fun register(id: PermissionId, description: String, base: PermissionId): P { + grantedPermissionsMap[id] = CopyOnWriteArrayList() // mutations are not quite often performed + val instance = createPermission(id, description, base) + val old = permissions.putIfAbsent(id, instance) + if (old != null) throw DuplicatedPermissionRegistrationException(instance, old) + return instance + } + + override fun grant(permissibleIdentifier: PermissibleIdentifier, permission: P) { + val id = permission.id + grantedPermissionsMap[id]?.add(permissibleIdentifier) + ?: error("Bad PermissionService implementation: grantedPermissionsMap[id] is null.") + } + + override fun deny(permissibleIdentifier: PermissibleIdentifier, permission: P) { + grantedPermissionsMap[permission.id]?.remove(permissibleIdentifier) + } + + override fun getRegisteredPermissions(): Sequence

= permissions.values.asSequence() + public override fun getGrantedPermissions(permissibleIdentifier: PermissibleIdentifier): Sequence

= sequence

{ + for ((permissionIdentifier, permissibleIdentifiers) in grantedPermissionsMap) { + + val granted = + if (permissibleIdentifiers.isEmpty()) false + else permissibleIdentifiers.any { permissibleIdentifier.grantedWith(it) } + + if (granted) get(permissionIdentifier)?.let { yield(it) } + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/BuiltInPermissionServices.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/BuiltInPermissionServices.kt new file mode 100644 index 000000000..de50373cb --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/BuiltInPermissionServices.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 + */ + +package net.mamoe.mirai.console.permission + +import net.mamoe.mirai.console.data.AutoSavePluginConfig +import net.mamoe.mirai.console.data.PluginDataExtensions.withDefault +import net.mamoe.mirai.console.data.value +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.createType + + +@ExperimentalPermission +public object AllGrantPermissionService : PermissionService { + private val all = ConcurrentHashMap() + override val permissionType: KClass + get() = PermissionImpl::class + + override fun register( + id: PermissionId, + description: String, + base: PermissionId + ): PermissionImpl { + val new = PermissionImpl(id, description, base) + val old = all.putIfAbsent(id, new) + if (old != null) throw DuplicatedPermissionRegistrationException(new, old) + return new + } + + override fun get(id: PermissionId): PermissionImpl? = all[id] + override fun getRegisteredPermissions(): Sequence = all.values.asSequence() + override fun getGrantedPermissions(permissibleIdentifier: PermissibleIdentifier): Sequence = + all.values.asSequence() + + override fun grant(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl) { + } + + override fun testPermission(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl): Boolean = + true + + override fun deny(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl) { + } +} + +@ExperimentalPermission +public object AllDenyPermissionService : PermissionService { + private val all = ConcurrentHashMap() + override val permissionType: KClass + get() = PermissionImpl::class + + override fun register( + id: PermissionId, + description: String, + base: PermissionId + ): PermissionImpl { + val new = PermissionImpl(id, description, base) + val old = all.putIfAbsent(id, new) + if (old != null) throw DuplicatedPermissionRegistrationException(new, old) + return new + } + + override fun get(id: PermissionId): PermissionImpl? = all[id] + override fun getRegisteredPermissions(): Sequence = all.values.asSequence() + override fun getGrantedPermissions(permissibleIdentifier: PermissibleIdentifier): Sequence = + emptySequence() + + override fun grant(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl) { + } + + override fun testPermission(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl): Boolean = + false + + override fun deny(permissibleIdentifier: PermissibleIdentifier, permission: PermissionImpl) { + } +} + +@ExperimentalPermission +internal object BuiltInPermissionService : AbstractConcurrentPermissionService(), + PermissionService { + + @ExperimentalPermission + override val permissionType: KClass + get() = PermissionImpl::class + override val permissions: MutableMap = ConcurrentHashMap() + + @Suppress("UNCHECKED_CAST") + override val grantedPermissionsMap: MutableMap> + get() = config.grantedPermissionMap as MutableMap> + + override fun createPermission(id: PermissionId, description: String, base: PermissionId): PermissionImpl = + PermissionImpl(id, description, base) + + internal val config: ConcurrentSaveData = + ConcurrentSaveData( + PermissionImpl::class.createType(), + "PermissionService", + + ) + + @Suppress("RedundantVisibilityModifier") + @ExperimentalPermission + internal class ConcurrentSaveData

private constructor( + permissionType: KType, + public override val saveName: String, + // delegate: PluginConfig, + @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any? + ) : AutoSavePluginConfig() { + public val grantedPermissionMap: MutableMap> + by value>>(ConcurrentHashMap()) + .withDefault { CopyOnWriteArrayList() } + + public companion object { + @JvmStatic + public operator fun

invoke( + permissionType: KType, + saveName: String, + // delegate: PluginConfig, + ): ConcurrentSaveData

= ConcurrentSaveData(permissionType, saveName, null) + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/DuplicatedPermissionRegistrationException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/DuplicatedPermissionRegistrationException.kt new file mode 100644 index 000000000..39cbb2807 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/DuplicatedPermissionRegistrationException.kt @@ -0,0 +1,18 @@ +/* + * 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", "MemberVisibilityCanBePrivate", "CanBeParameter") + +package net.mamoe.mirai.console.permission + +@ExperimentalPermission +public class DuplicatedPermissionRegistrationException( + newInstance: Permission, + public val existingInstance: Permission +) : Exception("Duplicated Permission registry. new: $newInstance, existing: $existingInstance") \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/ExperimentalPermission.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/ExperimentalPermission.kt new file mode 100644 index 000000000..4e0252a7e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/ExperimentalPermission.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +import kotlin.annotation.AnnotationTarget.* + +/** + * 标记一个实验性的权限系统 API. + * + * 权限系统是在 1.0-M4 引入的一个实验性系统, 目前不具有 API 稳定性. + */ +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) +@MustBeDocumented +public annotation class ExperimentalPermission( + val message: String = "" +) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permissible.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permissible.kt new file mode 100644 index 000000000..b970e50e5 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permissible.kt @@ -0,0 +1,28 @@ +/* + * 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", "unused", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.permission + +import net.mamoe.mirai.console.command.CommandSender + +/** + * 可拥有权限的对象. + * + * 典型的实例为 [CommandSender] + * + * 注意: 请不要自主实现 [Permissible] + * + * @see CommandSender + */ +@ExperimentalPermission +public interface Permissible { + public val identifier: PermissibleIdentifier +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissibleIdentifier.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissibleIdentifier.kt new file mode 100644 index 000000000..e65719740 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissibleIdentifier.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console.permission + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.internal.data.map +import net.mamoe.mirai.console.util.ConsoleExperimentalAPI + +/** + */ +@ExperimentalPermission("Classname is subject to change") +public interface PermissibleIdentifier { + public val parents: Array + + public companion object { + @ExperimentalPermission + public fun PermissibleIdentifier.grantedWith(with: PermissibleIdentifier): Boolean { + return allParentsWithSelf().any { it == with } + } + + private fun PermissibleIdentifier.allParentsWithSelf(): Sequence { + return sequence { + yield(this@allParentsWithSelf) + yieldAll(parents.asSequence()) + } + } + } +} + +@Serializable(with = AbstractPermissibleIdentifier.AsStringSerializer::class) +@ExperimentalPermission +public sealed class AbstractPermissibleIdentifier( + public final override vararg val parents: PermissibleIdentifier +) : PermissibleIdentifier { + public companion object { + @JvmStatic + public fun parseFromString(string: String): AbstractPermissibleIdentifier { + val str = string.trim() + objects.find { it.toString() == str }?.let { return it as AbstractPermissibleIdentifier } + for ((regex, block) in regexes) { + val result = regex.find(str) ?: continue + if (result.range.last != str.lastIndex) continue + if (result.range.first != 0) continue + return result.destructured.run(block) + } + error("Cannot deserialize '$str' as AbstractPermissibleIdentifier") + } + + internal val objects by lazy { + // https://youtrack.jetbrains.com/issue/KT-41782 + AbstractPermissibleIdentifier::class.nestedClasses.mapNotNull { it.objectInstance } + } + + internal val regexes: List AbstractPermissibleIdentifier>> = + listOf( + Regex("""ExactGroup\(\s*([0-9]+)\s*\)""") to { (id) -> ExactGroup(id.toLong()) }, + Regex("""ExactFriend\(\s*([0-9]+)\s*\)""") to { (id) -> ExactFriend(id.toLong()) }, + Regex("""ExactUser\(\s*([0-9]+)\s*\)""") to { (id) -> ExactUser(id.toLong()) }, + Regex("""AnyMember\(\s*([0-9]+)\s*\)""") to { (id) -> AnyMember(id.toLong()) }, + Regex("""ExactMember\(\s*([0-9]+)\s*([0-9]+)\s*\)""") to { (a, b) -> + ExactMember( + a.toLong(), + b.toLong() + ) + }, + Regex("""ExactTemp\(\s*([0-9]+)\s*([0-9]+)\s*\)""") to { (a, b) -> ExactTemp(a.toLong(), b.toLong()) }, + ) + } + + @ConsoleExperimentalAPI + public object AsStringSerializer : KSerializer by String.serializer().map( + serializer = { it.toString() }, + + deserializer = d@{ str -> parseFromString(str) } + ) + + public object AnyGroup : AbstractPermissibleIdentifier(AnyContact) { + override fun toString(): String = "AnyGroup" + } + + public data class ExactGroup(public val groupId: Long) : AbstractPermissibleIdentifier(AnyGroup) + + public data class AnyMember(public val groupId: Long) : AbstractPermissibleIdentifier(AnyMemberFromAnyGroup) + + public object AnyMemberFromAnyGroup : AbstractPermissibleIdentifier(AnyUser) { + override fun toString(): String = "AnyMemberFromAnyGroup" + } + + public data class ExactMember( + public val groupId: Long, + public val memberId: Long + ) : AbstractPermissibleIdentifier(AnyMember(groupId), ExactUser(memberId)) + + public object AnyFriend : AbstractPermissibleIdentifier(AnyUser) { + override fun toString(): String = "AnyFriend" + } + + public data class ExactFriend( + public val id: Long + ) : AbstractPermissibleIdentifier(ExactUser(id)) { + override fun toString(): String = "ExactFriend" + } + + public object AnyTemp : AbstractPermissibleIdentifier(AnyUser) { + override fun toString(): String = "AnyTemp" + } + + public data class ExactTemp( + public val groupId: Long, + public val id: Long + ) : AbstractPermissibleIdentifier(ExactUser(groupId)) // TODO: 2020/9/8 ExactMember ? + + public object AnyUser : AbstractPermissibleIdentifier(AnyContact) { + override fun toString(): String = "AnyUser" + } + + public data class ExactUser( + public val id: Long + ) : AbstractPermissibleIdentifier(AnyUser) + + public object AnyContact : AbstractPermissibleIdentifier() { + override fun toString(): String = "AnyContact" + } + + public object Console : AbstractPermissibleIdentifier() { + override fun toString(): String = "Console" + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt new file mode 100644 index 000000000..79ca00b77 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/Permission.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.console.permission.PermissionService.Companion.findCorrespondingPermission +import net.mamoe.mirai.console.util.ConsoleExperimentalAPI + + +/** + * 一个权限节点. + * + * 由 [PermissionService] 实现不同, [Permission] 可能会有多种实例. 但一个权限总是拥有确定的 [id]. + * + * 请不要手动实现这个接口. 总是从 [PermissionService.register] 获得实例. + */ +@ExperimentalPermission +public interface Permission { + public val id: PermissionId + public val description: String + public val parentId: PermissionId +} + +@OptIn(ExperimentalPermission::class) +private val ROOT_PERMISSION_ID = PermissionId("*", "*") + +/** + * 所有权限的父权限. + */ +@get:JvmName("getRootPermission") +@ExperimentalPermission +public val RootPermission: Permission by lazy { + PermissionService.INSTANCE.register( + ROOT_PERMISSION_ID, + "The parent of any permission", + ROOT_PERMISSION_ID + ) +} + +@ConsoleExperimentalAPI +@ExperimentalPermission +public fun Permission.parentsWithSelfSequence(): Sequence = + generateSequence(this) { p -> + p.parentId.findCorrespondingPermission()?.takeIf { parent -> parent != p } + } + +/** + * [Permission] 的简单实现 + */ +@Serializable +@ExperimentalPermission +public class PermissionImpl( + override val id: PermissionId, + override val description: String, + override val parentId: PermissionId = RootPermission.id +) : Permission \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt new file mode 100644 index 000000000..d4362a5ae --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionId.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.internal.data.map + + +/** + * [PermissionId] 与 [Permission] 唯一对应. + */ +@Serializable(with = PermissionId.AsStringSerializer::class) +@ExperimentalPermission +public data class PermissionId( + public val namespace: String, + public val id: String +) { + init { + require(!namespace.contains(':')) { + "':' is not allowed in namespace" + } + require(!id.contains(':')) { + "':' is not allowed in id" + } + } + + @Serializer(forClass = PermissionId::class) + public object AsClassSerializer + + public object AsStringSerializer : KSerializer by String.serializer().map( + serializer = { it.namespace + ":" + it.id }, + deserializer = { it.split(':').let { (namespace, id) -> PermissionId(namespace, id) } } + ) + + public override fun toString(): String { + return "$namespace:$id" + } + + public companion object { + public fun parseFromString(string: String): PermissionId = + string.split(':').let { (namespace, id) -> PermissionId(namespace, id) } + } +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt new file mode 100644 index 000000000..6ce0a93a5 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionIdNamespace.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +@ExperimentalPermission +public interface PermissionIdNamespace { + @ExperimentalPermission + public fun permissionId(id: String): PermissionId +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionNotFoundException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionNotFoundException.kt new file mode 100644 index 000000000..e005fd0aa --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionNotFoundException.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.permission + +@ExperimentalPermission +public open class PermissionNotFoundException public constructor( + public val id: PermissionId +) : Exception(id.toString()) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt index 0d01df6b8..42e30cb20 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/permission/PermissionService.kt @@ -7,28 +7,145 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("NOTHING_TO_INLINE", "unused", "MemberVisibilityCanBePrivate") + package net.mamoe.mirai.console.permission -import net.mamoe.mirai.console.extension.useExtensions +import net.mamoe.mirai.console.extension.SingletonExtensionPoint.Companion.findSingleton import net.mamoe.mirai.console.extensions.PermissionServiceProvider -import net.mamoe.mirai.console.util.ConsoleExperimentalAPI +import kotlin.reflect.KClass +import kotlin.reflect.full.isSuperclassOf /** * [PermissionServiceProvider] */ -@ConsoleExperimentalAPI -public interface PermissionService { +@ExperimentalPermission +public interface PermissionService

{ + @ExperimentalPermission + public val permissionType: KClass

+ + /////////////////////////////////////////////////////////////////////////// + + public operator fun get(id: PermissionId): P? + + public fun getRegisteredPermissions(): Sequence

+ public fun getGrantedPermissions(permissibleIdentifier: PermissibleIdentifier): Sequence

+ + public fun testPermission(permissibleIdentifier: PermissibleIdentifier, permission: P): Boolean { + val permissionId = permission.id + val all = this[permissionId]?.parentsWithSelfSequence() ?: return false + return getGrantedPermissions(permissibleIdentifier).any { p -> + all.any { p.id == it.id } + } + } - public companion object : PermissionService { - private val instance by lazy { - PermissionServiceProvider.useExtensions { } + /////////////////////////////////////////////////////////////////////////// + + @Throws(DuplicatedPermissionRegistrationException::class) + public fun register( + id: PermissionId, + description: String, + base: PermissionId = RootPermission.id + ): P + + /////////////////////////////////////////////////////////////////////////// + + public fun grant(permissibleIdentifier: PermissibleIdentifier, permission: P) + public fun deny(permissibleIdentifier: PermissibleIdentifier, permission: P) + + public companion object { + @get:JvmName("getInstance") + @JvmStatic + public val INSTANCE: PermissionService by lazy { + PermissionServiceProvider.findSingleton()?.instance ?: BuiltInPermissionService } + public fun

PermissionService

.getOrFail(id: PermissionId): P = + get(id) ?: throw PermissionNotFoundException(id) + + internal fun PermissionService<*>.allocatePermissionIdForPlugin(name: String, id: String) = + PermissionId("plugin.${name.toLowerCase()}", id.toLowerCase()) + + public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this] + + public fun PermissionId.findCorrespondingPermissionOrFail(): Permission = INSTANCE.getOrFail(this) + + public fun PermissibleIdentifier.grantPermission(permission: Permission) { + INSTANCE.checkType(permission::class).grant(this, permission) + } + + public fun PermissibleIdentifier.grantPermission(permissionId: PermissionId) { + grantPermission(permissionId.findCorrespondingPermissionOrFail()) + } + + public fun PermissibleIdentifier.denyPermission(permission: Permission) { + INSTANCE.checkType(permission::class).deny(this, permission) + } + + public fun PermissibleIdentifier.denyPermission(permissionId: PermissionId) { + denyPermission(permissionId.findCorrespondingPermissionOrFail()) + } + + public fun Permissible.hasPermission(permission: Permission): Boolean = + permission.testPermission(this@hasPermission) + + public fun PermissibleIdentifier.hasPermission(permission: Permission): Boolean = + permission.testPermission(this@hasPermission) + + public fun PermissibleIdentifier.hasPermission(permissionId: PermissionId): Boolean { + val instance = permissionId.findCorrespondingPermissionOrFail() + return INSTANCE.checkType(instance::class).testPermission(this@hasPermission, instance) + } + + public fun Permissible.hasPermission(permissionId: PermissionId): Boolean = + permissionId.testPermission(this@hasPermission) + + public fun Permissible.getGrantedPermissions(): Sequence = + INSTANCE.getGrantedPermissions(this@getGrantedPermissions.identifier) + + public fun Permissible.grantPermission(vararg permissions: Permission) { + for (permission in permissions) { + INSTANCE.checkType(permission::class).grant(this.identifier, permission) + } + } + + public fun Permissible.denyPermission(vararg permissions: Permission) { + for (permission in permissions) { + INSTANCE.checkType(permission::class).deny(this.identifier, permission) + } + } + + public fun PermissibleIdentifier.getGrantedPermissions(): Sequence = + INSTANCE.getGrantedPermissions(this@getGrantedPermissions) + + public fun Permission.testPermission(permissible: Permissible): Boolean = + INSTANCE.checkType(this::class).testPermission(permissible.identifier, this@testPermission) + + public fun Permission.testPermission(permissibleIdentifier: PermissibleIdentifier): Boolean = + INSTANCE.checkType(this::class).testPermission(permissibleIdentifier, this@testPermission) + + public fun PermissionId.testPermission(permissible: Permissible): Boolean { + val p = INSTANCE[this] ?: return false + return p.testPermission(permissible) + } + + public fun PermissionId.testPermission(permissible: PermissibleIdentifier): Boolean { + val p = INSTANCE[this] ?: return false + return p.testPermission(permissible) + } } } -public interface proprietary +@OptIn(ExperimentalPermission::class) +internal fun PermissionService<*>.checkType(permissionType: KClass): PermissionService { + return PermissionService.INSTANCE.run { + require(this.permissionType.isSuperclassOf(permissionType)) { + "Custom-constructed Permission instance is not allowed (Required ${this.permissionType}, found ${permissionType}. " + + "Please obtain Permission from PermissionService.INSTANCE.register or PermissionService.INSTANCE.get" + } -@ConsoleExperimentalAPI -public interface Permission + @Suppress("UNCHECKED_CAST") + this as PermissionService + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt index ec09365fa..a9248bb7d 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt @@ -82,4 +82,4 @@ public inline val Plugin.author: String get() = this.description.author /** * 获取 [PluginDescription.dependencies] */ -public inline val Plugin.dependencies: List get() = this.description.dependencies +public inline val Plugin.dependencies: Set get() = this.description.dependencies diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/IllegalPluginDescriptionException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/IllegalPluginDescriptionException.kt new file mode 100644 index 000000000..1083259ce --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/IllegalPluginDescriptionException.kt @@ -0,0 +1,8 @@ +package net.mamoe.mirai.console.plugin.description + +public class IllegalPluginDescriptionException : RuntimeException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt index 0e4dc7802..957a14bf7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt @@ -18,9 +18,11 @@ import com.vdurmont.semver4j.Semver * * @see PluginDescription.dependencies */ -public data class PluginDependency( - /** 依赖插件名 */ - public val name: String, +public data class PluginDependency @JvmOverloads constructor( + /** + * 依赖插件 ID, [PluginDescription.id] + */ + public val id: String, /** * 依赖版本号. 为 null 时则为不限制版本. * @@ -34,6 +36,16 @@ public data class PluginDependency( */ public val isOptional: Boolean = false ) { + /** + * @see PluginDependency + */ + public constructor(name: String, isOptional: Boolean = false) : this( + name, null, isOptional + ) + + /** + * @see PluginDependency + */ public constructor(name: String, version: String, isOptional: Boolean) : this( name, Semver(version, Semver.SemverType.IVY), diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt index a5f259502..6ef60f81a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDescription.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.console.plugin.description import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.plugin.PluginLoadException /** @@ -27,7 +28,44 @@ public interface PluginDescription { public val kind: PluginKind /** - * 插件名称. 不允许存在 ":" + * 插件 ID, 必须全英文, 仅允许英文字母, '-', '_', '.'. + * + * - 类似于 Java 包名, 插件 ID 需要 '域名.名称' 格式, 如 `net.mamoe.mirai.example-plugin` + * - 域名和名称都是必须的 + * - '.' 不允许位于首位或末尾 + * - '-' 和 '_' 仅允许存在于两个英文字母之间 + * + * ID 在插件发布后就应该保持不变, 以便其他插件添加依赖. + * + * 插件 ID 的域名和名称都不能完全是以下其中一个 ([FORBIDDEN_ID_WORDS]). + * - "console" + * - "main" + * - "plugin" + * - "config" + * - "data" + * + * + * ID 用于指令权限等一些内部处理 + * + * @see FORBIDDEN_ID_LETTERS + * @see FORBIDDEN_ID_WORDS + */ + public val id: String + + /** + * 插件名称. 允许中文, 允许各类符号. + * + * 插件名称不能完全是以下其中一种 ([FORBIDDEN_ID_WORDS]). + * - console + * - main + * - plugin + * - config + * - data + * + * 插件名称用于显示给用户. + * + * @see FORBIDDEN_ID_LETTERS + * @see FORBIDDEN_ID_WORDS */ public val name: String @@ -39,7 +77,21 @@ public interface PluginDescription { /** * 插件版本. * - * 语法参考: ([语义化版本 2.0.0](https://semver.org/lang/zh-CN/)) + * 语法参考: ([语义化版本 2.0.0](https://semver.org/lang/zh-CN/)). + * + * 合法的版本号示例: + * - `1.0.0` + * - `1.0` + * - `1.0-M1` + * - `1.0.0-M1` + * - `1.0.0-M2-1` + * - `1` (尽管非常不建议这么做) + * + * 非法版本号实例: + * - `DEBUG-1` + * - `-1.0` + * - `v1.0` (不允许 "v") + * - `V1.0` (不允许 "V") * * @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本. */ @@ -55,6 +107,77 @@ public interface PluginDescription { * * @see PluginDependency */ - public val dependencies: List + public val dependencies: Set + + public companion object { + public val FORBIDDEN_ID_LETTERS: Array = "~!@#$%^&*()+/*<>{}|[]\\?".map(Char::toString).toTypedArray() + public val FORBIDDEN_ID_WORDS: Array = arrayOf("main", "console", "plugin", "config", "data") + + /** + * 依次检查 [PluginDescription] 的 [PluginDescription.id], [PluginDescription.name], [PluginDescription.dependencies] 的合法性 + * + * @throws IllegalPluginDescriptionException 当不合法时抛出. + */ + @Throws(IllegalPluginDescriptionException::class) + public fun checkPluginDescription(instance: PluginDescription) { + kotlin.runCatching { + checkPluginId(instance.id) + checkPluginName(instance.name) + checkDependencies(instance.id, instance.dependencies) + }.getOrElse { + throw IllegalPluginDescriptionException( + "Illegal description. Plugin ${instance.name} (${instance.id})", + it + ) + } + } + + /** + * 检查 [PluginDescription.id] 的合法性. + * + * @throws IllegalPluginDescriptionException 当不合法时抛出. + */ + @Throws(IllegalPluginDescriptionException::class) + public fun checkPluginId(id: String) { + if (id.isBlank()) throw IllegalPluginDescriptionException("Plugin id cannot be blank") + if (id.count { it == '.' } < 2) throw IllegalPluginDescriptionException("'$id' is illegal. Plugin id must consist of both domain and name. ") + + FORBIDDEN_ID_LETTERS.firstOrNull { it in id }?.let { illegal -> + throw IllegalPluginDescriptionException("Plugin id contains illegal char: $illegal.") + } + + val idSections = id.split('.') + FORBIDDEN_ID_WORDS.firstOrNull { it in idSections }?.let { illegal -> + throw IllegalPluginDescriptionException("Plugin id contains illegal word: '$illegal'.") + } + } + + /** + * 检查 [PluginDescription.name] 的合法性. + * + * @throws IllegalPluginDescriptionException 当不合法时抛出. + */ + @Throws(IllegalPluginDescriptionException::class) + public fun checkPluginName(name: String) { + if (name.isBlank()) throw IllegalPluginDescriptionException("Plugin name cannot be blank") + FORBIDDEN_ID_WORDS.firstOrNull { it in name }?.let { illegal -> + throw IllegalPluginDescriptionException("Plugin name is illegal: '$illegal'.") + } + } + + /** + * 检查 [PluginDescription.dependencies] 的合法性. + * + * @throws IllegalPluginDescriptionException 当不合法时抛出. + */ + @Throws(IllegalPluginDescriptionException::class) + public fun checkDependencies(pluginId: String, dependencies: Set) { + if (dependencies.distinctBy { it.id }.size != dependencies.size) + throw PluginLoadException("Duplicated dependency detected: A plugin cannot depend on different versions of dependencies of the same id") + + if (dependencies.any { it.id == pluginId }) + throw PluginLoadException("Recursive dependency detected: A plugin cannot depend on itself") + } + } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginKind.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginKind.kt index a2fc7e6b7..fce47b97b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginKind.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginKind.kt @@ -10,19 +10,34 @@ package net.mamoe.mirai.console.plugin.description import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.extension.Extension +import net.mamoe.mirai.console.extensions.BotConfigurationAlterer +import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.description.PluginKind.* /** - * 插件类型 + * 插件类型. + * + * 插件类型将影响加载顺序: [LOADER] -> [HIGH_PRIORITY_EXTENSIONS] -> [NORMAL]. + * + * 依赖解决过程与插件类型有很大关联. 在一个较早的阶段, 只会解决在此阶段加载的插件. 意味着 [LOADER] 不允许依赖一个 [NORMAL] 类型的插件. */ -@Serializable(with = PluginKind.AsStringSerializer::class) public enum class PluginKind { - /** 表示此插件提供一个 [PluginLoader], 应在加载其他 [NORMAL] 类型插件前加载 */ + /** 表示此插件提供一个 [PluginLoader], 也可以同时提供其他 [Extension] 应最早被加载 */ LOADER, + /** + * 表示此插件提供一些高优先级的 [Extension], 应在加载其他 [NORMAL] 类型插件前加载 + * + * 高优先级的 [Extension] 通常是覆盖 Console 内置的部分服务的扩展. 如 [PermissionServiceProvider]. + * + * 一些普通的 [Extension], 如 [BotConfigurationAlterer], 也可以使用 [NORMAL] 类型插件注册. + */ + HIGH_PRIORITY_EXTENSIONS, + /** 表示此插件为一个通常的插件, 按照正常的依赖关系加载. */ NORMAL; diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt index 85df5ea00..b6ce680a7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.CoroutineScope import net.mamoe.mirai.console.data.AutoSavePluginDataHolder import net.mamoe.mirai.console.data.PluginConfig import net.mamoe.mirai.console.data.PluginData +import net.mamoe.mirai.console.permission.ExperimentalPermission +import net.mamoe.mirai.console.permission.PermissionIdNamespace import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.PluginFileExtensions import net.mamoe.mirai.console.plugin.ResourceContainer @@ -41,8 +43,9 @@ import net.mamoe.mirai.utils.MiraiLogger * @see JvmPlugin 支持文件系统扩展 * @see ResourceContainer 支持资源获取 (如 Jar 中的资源文件) */ +@OptIn(ExperimentalPermission::class) public interface JvmPlugin : Plugin, CoroutineScope, - PluginFileExtensions, ResourceContainer, AutoSavePluginDataHolder { + PluginFileExtensions, ResourceContainer, AutoSavePluginDataHolder, PermissionIdNamespace { /** 日志 */ public val logger: MiraiLogger diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt index 66e554c6a..363f356df 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("unused") +@file:Suppress("unused", "INVISIBLE_REFERENCE", "INVISIBLE_member") package net.mamoe.mirai.console.plugin.jvm @@ -15,37 +15,250 @@ import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginKind +import kotlin.internal.LowPriorityInOverloadResolution /** * JVM 插件的描述. 通常作为 `plugin.yml` + * + * 请不要自行实现 [JvmPluginDescription] 接口. 它不具有继承稳定性. + * * @see SimpleJvmPluginDescription + * @see JvmPluginDescriptionBuilder */ -public interface JvmPluginDescription : PluginDescription +public interface JvmPluginDescription : PluginDescription { + public companion object { + /** + * 构建 [JvmPluginDescription] + * @see JvmPluginDescriptionBuilder + */ + @JvmSynthetic + public operator fun invoke( + name: String, + version: String, + block: JvmPluginDescriptionBuilder.() -> Unit = {} + ): JvmPluginDescription = JvmPluginDescriptionBuilder(name, version).apply(block).build() + + /** + * 构建 [JvmPluginDescription] + * @see JvmPluginDescriptionBuilder + */ + @JvmSynthetic + public operator fun invoke( + name: String, + version: Semver, + block: JvmPluginDescriptionBuilder.() -> Unit = {} + ): JvmPluginDescription = JvmPluginDescriptionBuilder(name, version).apply(block).build() + } +} /** + * [JvmPluginDescription] 构建器. + * + * #### Kotlin Example + * ``` + * val desc = JvmPluginDescription("org.example.example-plugin", "1.0.0") { + * info("This is an example plugin") + * dependsOn("org.example.another-plugin") + * } + * ``` + * + * #### Java Example + * ``` + * JvmPluginDescription desc = new JvmPluginDescriptionBuilder("org.example.example-plugin", "1.0.0") + * .info("This is an example plugin") + * .dependsOn("org.example.another-plugin") + * .build() + * ``` + * + * @see [JvmPluginDescription.invoke] + */ +public class JvmPluginDescriptionBuilder( + private var id: String, + private var version: Semver, +) { + public constructor(name: String, version: String) : this(name, Semver(version, Semver.SemverType.LOOSE)) + + private var name: String = id + private var author: String = "" + private var info: String = "" + private var dependencies: MutableSet = mutableSetOf() + private var kind: PluginKind = PluginKind.NORMAL + + @ILoveKuriyamaMiraiForever + public fun name(value: String): JvmPluginDescriptionBuilder = apply { this.name = value.trim() } + + @ILoveKuriyamaMiraiForever + public fun version(value: String): JvmPluginDescriptionBuilder = + apply { this.version = Semver(value, Semver.SemverType.LOOSE) } + + @ILoveKuriyamaMiraiForever + public fun version(value: Semver): JvmPluginDescriptionBuilder = apply { this.version = value } + + @ILoveKuriyamaMiraiForever + public fun id(value: String): JvmPluginDescriptionBuilder = apply { this.id = value.trim() } + + @ILoveKuriyamaMiraiForever + public fun author(value: String): JvmPluginDescriptionBuilder = apply { this.author = value.trim() } + + @ILoveKuriyamaMiraiForever + public fun info(value: String): JvmPluginDescriptionBuilder = apply { this.info = value.trimIndent() } + + @ILoveKuriyamaMiraiForever + public fun kind(value: PluginKind): JvmPluginDescriptionBuilder = apply { this.kind = value } + + @ILoveKuriyamaMiraiForever + public fun normalPlugin(): JvmPluginDescriptionBuilder = apply { this.kind = PluginKind.NORMAL } + + @ILoveKuriyamaMiraiForever + public fun loaderProviderPlugin(): JvmPluginDescriptionBuilder = apply { this.kind = PluginKind.LOADER } + + @ILoveKuriyamaMiraiForever + public fun highPriorityExtensionsPlugin(): JvmPluginDescriptionBuilder = + apply { this.kind = PluginKind.HIGH_PRIORITY_EXTENSIONS } + + @ILoveKuriyamaMiraiForever + public fun dependsOn( + pluginId: String, + version: String? = null, + isOptional: Boolean = false + ): JvmPluginDescriptionBuilder = apply { + if (version == null) this.dependencies.add(PluginDependency(pluginId, version, isOptional)) + else this.dependencies.add(PluginDependency(pluginId, version, isOptional)) + } + + @ILoveKuriyamaMiraiForever + public fun setDependencies( + value: Set + ): JvmPluginDescriptionBuilder = apply { + this.dependencies = value.toMutableSet() + } + + @ILoveKuriyamaMiraiForever + public fun dependsOn( + vararg dependencies: PluginDependency + ): JvmPluginDescriptionBuilder = apply { + for (dependency in dependencies) { + this.dependencies.add(dependency) + } + } + + @ILoveKuriyamaMiraiForever + public fun dependsOn( + pluginId: String, + version: Semver? = null, + isOptional: Boolean = false + ): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, version, isOptional)) } + + + @Suppress("DEPRECATION_ERROR") + public fun build(): JvmPluginDescription = + SimpleJvmPluginDescription(name, version, id, author, info, dependencies, kind) + + @Retention(AnnotationRetention.SOURCE) + @DslMarker + private annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5 +} + +/** + * @constructor 推荐使用带名称的参数, 而不要按位置摆放. + * * @see JvmPluginDescription */ +@Deprecated( + """ + 将在 1.0-RC 删除. 请使用 JvmPluginDescription. +""", + replaceWith = ReplaceWith( + "JvmPluginDescription", + "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription" + ), + level = DeprecationLevel.ERROR +) public data class SimpleJvmPluginDescription +@Deprecated( + """ + 构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder. +""", + replaceWith = ReplaceWith( + "JvmPluginDescription(name, version) {}", + "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke" + ), + level = DeprecationLevel.ERROR +) @JvmOverloads public constructor( public override val name: String, public override val version: Semver, + public override val id: String = name, public override val author: String = "", public override val info: String = "", - public override val dependencies: List = listOf(), + public override val dependencies: Set = setOf(), public override val kind: PluginKind = PluginKind.NORMAL, ) : JvmPluginDescription { + @Deprecated( + """ + 构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder. +""", + replaceWith = ReplaceWith( + "JvmPluginDescription.invoke(name, version) {}", + "net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke" + ), + level = DeprecationLevel.ERROR + ) + @Suppress("DEPRECATION_ERROR") @JvmOverloads public constructor( name: String, version: String, + id: String = name, author: String = "", info: String = "", - dependencies: List = listOf(), + dependencies: Set = setOf(), kind: PluginKind = PluginKind.NORMAL, - ) : this(name, Semver(version, Semver.SemverType.LOOSE), author, info, dependencies, kind) + ) : this(name, Semver(version, Semver.SemverType.LOOSE), id, author, info, dependencies, kind) init { require(!name.contains(':')) { "':' is forbidden in plugin name" } } -} \ No newline at end of file +} + + +@Deprecated( + "JvmPluginDescription 没有构造器. 请使用 SimpleJvmPluginDescription.", + replaceWith = ReplaceWith( + "SimpleJvmPluginDescription(name, version, author, info, dependencies, kind)", + "net.mamoe.mirai.console.plugin.jvm.SimpleJvmPluginDescription" + ), + level = DeprecationLevel.WARNING +) +@LowPriorityInOverloadResolution +@Suppress("DEPRECATION_ERROR", "FunctionName") +public fun JvmPluginDescription( + name: String, + version: Semver, + id: String = name, + author: String = "", + info: String = "", + dependencies: Set = setOf(), + kind: PluginKind = PluginKind.NORMAL +): JvmPluginDescription = SimpleJvmPluginDescription(name, version, id, author, info, dependencies, kind) + +@Deprecated( + "JvmPluginDescription 没有构造器. 请使用 SimpleJvmPluginDescription.", + replaceWith = ReplaceWith( + "SimpleJvmPluginDescription(name, version, author, info, dependencies, kind)", + "net.mamoe.mirai.console.plugin.jvm.SimpleJvmPluginDescription" + ), + level = DeprecationLevel.WARNING +) +@LowPriorityInOverloadResolution +@Suppress("DEPRECATION_ERROR", "FunctionName") +public fun JvmPluginDescription( + name: String, + version: String, + id: String = name, + author: String = "", + info: String = "", + dependencies: Set = setOf(), + kind: PluginKind = PluginKind.NORMAL +): JvmPluginDescription = SimpleJvmPluginDescription(name, version, id, author, info, dependencies, kind) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt deleted file mode 100644 index 6007e47df..000000000 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("NOTHING_TO_INLINE") -@file:JvmMultifileClass -@file:JvmName("ConsoleUtils") - -package net.mamoe.mirai.console.util - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.internal.data.builtins.BotManagerImpl -import net.mamoe.mirai.contact.User - -public interface BotManager { - /** - * 判断此用户是否为 console 管理员 - */ - public val User.isManager: Boolean - public val Bot.managers: List - - public fun Bot.removeManager(id: Long): Boolean - public fun Bot.addManager(id: Long): Boolean - - public companion object INSTANCE : BotManager { // kotlin import handler doesn't recognize delegation. - override fun Bot.addManager(id: Long): Boolean = BotManagerImpl.run { addManager(id) } - override fun Bot.removeManager(id: Long): Boolean = BotManagerImpl.run { removeManager(id) } - override val User.isManager: Boolean get() = BotManagerImpl.run { isManager } - override val Bot.managers: List get() = BotManagerImpl.run { managers } - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt index 3a44fb66a..3db1b0a52 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -189,7 +189,7 @@ internal class TestCommand { val composite = object : CompositeCommand( ConsoleCommandOwner, - "test", + "test22", overrideContext = buildCommandArgumentContext { add(object : CommandArgumentParser { override fun parse(raw: String, sender: CommandSender): MyClass { @@ -234,7 +234,7 @@ internal class TestCommand { simple.withRegistration { // assertEquals("xxx", withTesting { simple.execute(sender, "xxx") }) - assertEquals("xxx", withTesting { println(sender.executeCommand("/test xxx")) }) + assertEquals("xxx", withTesting { assertSuccess(sender.executeCommand("/test xxx")) }) } } } diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt index d5c01c6f9..861101fd1 100644 --- a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/data/SettingTest.kt @@ -10,8 +10,8 @@ package net.mamoe.mirai.console.data import kotlinx.serialization.json.Json +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin -import net.mamoe.mirai.console.plugin.jvm.SimpleJvmPluginDescription import net.mamoe.mirai.console.util.ConsoleInternalAPI import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -21,7 +21,7 @@ import kotlin.test.assertSame internal class PluginDataTest { object MyPlugin : KotlinPlugin( - SimpleJvmPluginDescription( + JvmPluginDescription( "1", "2" ) ) diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/permission/PermissionsBasicsTest.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/permission/PermissionsBasicsTest.kt new file mode 100644 index 000000000..f60845990 --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/permission/PermissionsBasicsTest.kt @@ -0,0 +1,11 @@ +package net.mamoe.mirai.console.permission + +import org.junit.jupiter.api.Test + +internal class PermissionsBasicsTest { + + @Test + fun parentsWithSelfSequence() { + + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 3970cc2a4..a93801e38 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -18,7 +18,7 @@ object Versions { const val core = "1.2.2" - const val console = "1.0-M4-dev-4" + const val console = "1.0-M4-dev-5" const val consoleGraphical = "0.0.7" const val consoleTerminal = "0.1.0" const val consolePure = console diff --git a/docs/README.md b/docs/README.md index ffb1db774..a46c1b636 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,39 @@ 欢迎来到 mirai-console 开发文档! +## 目录 + +- **[准备工作](#准备工作)** +- **[启动 Console](#Run.md)** + +### 后端插件开发基础 + +- 插件 - [Plugin 模块](Plugins.md) +- 指令 - [Command 模块](Commands.md) +- 存储 - [PluginData 模块](PluginData.md) +- 权限 - [Permission 模块](Permissions.md) + +### 后端插件开发进阶 + +- 扩展 - [Extension 模块和扩展点](Extensions.md) +- 扩展 - [实现 PluginLoader](PluginLoader.md) +- 扩展 - [实现 PermissionService](PermissionService.md) + +### 实现前端 +- [FrontEnd](FrontEnd.md) + +[`Plugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt +[`Annotations`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/Annotations.kt +[`PluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt +[`JavaPluginScheduler`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPluginScheduler.kt +[`JvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt +[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt +[`PluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt +[`ConsoleInput`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ConsoleInput.kt +[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt +[`BotManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt +[`Command`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt + ## 准备工作 ***如果跳过本节内容,你很可能遇到无法解决的问题。*** @@ -43,42 +76,12 @@ - 对于 Java 使用者,请阅读 [Java 用户的使用指南](#java-用户的使用指南),[在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数](#在-java-使用-mirai-console-中的-kotlin-suspend-函数) - 对于 Kotlin 使用者,请熟知 [Kotlin `1.4` 版本带来的新特性](#mirai-console-使用的-kotlin-14-版本的新特性) -## 目录 - -### 后端插件开发基础 - -- 插件 - [Plugin 模块](Plugins.md) -- 指令 - [Command 模块](Commands.md) -- 存储 - [PluginData 模块](PluginData.md) -- 权限 - [Permission 模块](Permissions.md) - -### 后端插件开发进阶 - -- 扩展 - [Extension 模块和扩展点](Extensions.md) -- 扩展 - [实现 PluginLoader](PluginLoader.md) -- 扩展 - [实现 PermissionService](PermissionService.md) - -### 实现前端 -- [FrontEnd](FrontEnd.md) - -[`Plugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt -[`Annotations`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/Annotations.kt -[`PluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt -[`JavaPluginScheduler`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPluginScheduler.kt -[`JvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt -[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt -[`PluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt -[`ConsoleInput`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ConsoleInput.kt -[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt -[`BotManager`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/BotManager.kt -[`Command`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt - ## 附录 ### Java 用户的使用指南 -- Java 中的「方法」在 Kotlin 中均被成为「函数」。 +- Java 中的「方法」在 Kotlin 中均被称为「函数」。 - Kotlin 默认的访问权限是 `public`。如 Kotlin `class Test` 相当于 Java 的 `public class Test {}` - Kotlin 的函数定义 `fun test(int: Int): String` 相当于 Java 的方法定义 `public String test(int int)` @@ -164,4 +167,4 @@ Mirai Console 的版本号遵循 [语义化版本 2.0.0](https://semver.org/lang 在 `1.2.0` 上升为 `ERROR`(使用时会得到一个编译错误); 在 `1.3.0` 上升为 `HIDDEN`(使用者无法看到这些 API)。 -`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。 \ No newline at end of file +`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。 diff --git a/docs/Run.md b/docs/Run.md new file mode 100644 index 000000000..435f45900 --- /dev/null +++ b/docs/Run.md @@ -0,0 +1,29 @@ +# Mirai Console - Run + +Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 + +## 使用第三方工具自动启动 + +## 独立启动 + +### 环境 +- JDK 11 + +### 准备文件 + +要启动 Mirai Console,你需要: +- mirai-core-qqandroid +- mirai-console 后端 +- mirai-console 任一前端 +- 相关依赖 + +只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 pure 前端可用。 + +### 启动 mirai-console-pure 前端 + +mirai 在版本发布时会同时发布打包依赖的 Shadow JAR,存放在 [mirai-repo]。 + +1. 在 [mirai-repo] 下载如下三个模块的最新版本文件: + - [] + +[mirai-repo]: https://github.com/project-mirai/mirai-repo/ diff --git a/frontend/mirai-console-pure/build.gradle.kts b/frontend/mirai-console-pure/build.gradle.kts index 9f9428196..3639b0a7b 100644 --- a/frontend/mirai-console-pure/build.gradle.kts +++ b/frontend/mirai-console-pure/build.gradle.kts @@ -1,7 +1,6 @@ plugins { kotlin("jvm") kotlin("plugin.serialization") - kotlin("kapt") id("java") `maven-publish` id("com.jfrog.bintray") @@ -46,10 +45,10 @@ dependencies { testApi(project(":mirai-console")) - val autoService = "1.0-rc7" - kapt("com.google.auto.service", "auto-service", autoService) - compileOnly("com.google.auto.service", "auto-service-annotations", autoService) - testCompileOnly("com.google.auto.service", "auto-service-annotations", autoService) +// val autoService = "1.0-rc7" +// kapt("com.google.auto.service", "auto-service", autoService) +// compileOnly("com.google.auto.service", "auto-service-annotations", autoService) +// testCompileOnly("com.google.auto.service", "auto-service-annotations", autoService) } ext.apply { diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt index c2f285cde..66408bb71 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.console.pure import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import net.mamoe.mirai.console.MiraiConsole @@ -28,7 +29,7 @@ val consoleLogger by lazy { DefaultLogger("console") } @OptIn(ConsoleInternalAPI::class) internal fun startupConsoleThread() { - MiraiConsole.launch { + MiraiConsole.launch(CoroutineName("Input")) { while (true) { try { val next = MiraiConsole.requestInput("").let { diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt index 0d6abc78f..9bdf2d2ce 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt @@ -74,6 +74,9 @@ class MiraiConsoleImplementationPure ) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope( NamedSupervisorJob("MiraiConsoleImplementationPure") + CoroutineExceptionHandler { coroutineContext, throwable -> + if (throwable is CancellationException) { + return@CoroutineExceptionHandler + } val coroutineName = coroutineContext[CoroutineName]?.name ?: "" MiraiConsole.mainLogger.error("Exception in coroutine $coroutineName", throwable) }) { diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt index 7f9686b6c..e52a0ef7e 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt @@ -92,11 +92,11 @@ internal object ConsoleCommandSenderImplPure : MiraiConsoleImplementation.Consol kotlin.runCatching { lineReader.printAbove(message) }.onFailure { - consoleLogger.error(it) + consoleLogger.error("Exception while ConsoleCommandSenderImplPure.sendMessage", it) } } override suspend fun sendMessage(message: Message) { - return sendMessage(message.contentToString()) + return sendMessage(message.toString()) } } \ No newline at end of file diff --git a/frontend/mirai-console-terminal/README.md b/frontend/mirai-console-terminal/README.md deleted file mode 100644 index bd7980bfe..000000000 --- a/frontend/mirai-console-terminal/README.md +++ /dev/null @@ -1,6 +0,0 @@ -### Mirai Console Terminal -支持windows/mac/linux -在terminal环境下的Console, 由控制台富文本实现简易UI -优点: 可以在linux环境下运行/简洁使用效率高 -缺点: 需要有略微的terminal知识 -所使用插件系统与graphical版本一致 可以来回切换 \ No newline at end of file diff --git a/frontend/mirai-console-terminal/build.gradle.kts b/frontend/mirai-console-terminal/build.gradle.kts deleted file mode 100644 index cea7f593a..000000000 --- a/frontend/mirai-console-terminal/build.gradle.kts +++ /dev/null @@ -1,49 +0,0 @@ -plugins { - id("kotlinx-serialization") - id("kotlin") - id("java") -} - - -apply(plugin = "com.github.johnrengelman.shadow") - -version = Versions.Mirai.console - -tasks.withType { - manifest { - attributes["Main-Class"] = "net.mamoe.mirai.console.MiraiConsoleTerminalLoader" - } -} - -kotlin { - sourceSets { - all { - - languageSettings.useExperimentalAnnotation("kotlin.Experimental") - languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") - languageSettings.progressiveMode = true - languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") - } - } -} - -dependencies { - compileOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}") - api(project(":mirai-console")) - api(group = "com.googlecode.lanterna", name = "lanterna", version = "3.0.2") -} -val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks -compileKotlin.kotlinOptions { - jvmTarget = "1.8" -} -val compileTestKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks -compileTestKotlin.kotlinOptions { - jvmTarget = "1.8" -} -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} -tasks.withType(JavaCompile::class.java) { - options.encoding = "UTF8" -} diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt deleted file mode 100644 index 215b452d4..000000000 --- a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console - -import com.googlecode.lanterna.SGR -import com.googlecode.lanterna.TerminalSize -import com.googlecode.lanterna.TextColor -import com.googlecode.lanterna.graphics.TextGraphics -import com.googlecode.lanterna.input.KeyStroke -import com.googlecode.lanterna.input.KeyType -import com.googlecode.lanterna.terminal.DefaultTerminalFactory -import com.googlecode.lanterna.terminal.Terminal -import com.googlecode.lanterna.terminal.swing.SwingTerminal -import com.googlecode.lanterna.terminal.swing.SwingTerminalFrame -import kotlinx.coroutines.* -import kotlinx.coroutines.io.ByteWriteChannel -import kotlinx.coroutines.io.close -import kotlinx.coroutines.io.jvm.nio.copyTo -import kotlinx.coroutines.io.reader -import kotlinx.io.core.use -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.MiraiConsoleTerminalFrontEnd.LoggerDrawer.cleanPage -import net.mamoe.mirai.console.MiraiConsoleTerminalFrontEnd.LoggerDrawer.drawLog -import net.mamoe.mirai.console.MiraiConsoleTerminalFrontEnd.LoggerDrawer.redrawLogs -import net.mamoe.mirai.console.command.CommandManager -import net.mamoe.mirai.console.command.ConsoleCommandSender -import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd -import net.mamoe.mirai.utils.LoginSolver -import net.mamoe.mirai.utils.SimpleLogger.LogPriority -import java.awt.Image -import java.awt.image.BufferedImage -import java.io.File -import java.io.OutputStream -import java.io.PrintStream -import java.io.RandomAccessFile -import java.nio.ByteBuffer -import java.nio.charset.Charset -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import javax.imageio.ImageIO -import kotlin.concurrent.thread -import kotlin.coroutines.CoroutineContext -import kotlin.system.exitProcess - -/** - * 此文件不推荐任何人看 - * 可能导致 - * 1:心肌梗死 - * 2:呼吸困难 - * 3:想要重写但是发现改任何一个看似不合理的地方都会崩 - * - * @author NaturalHG - * - */ - -val String.actualLength: Int get() = this.sumBy { if (it.isChineseChar) 2 else 1 } - - -fun String.getSubStringIndexByActualLength(widthMax: Int): Int { - return this.sumBy { if (it.isChineseChar) 2 else 1 }.coerceAtMost(widthMax).coerceAtLeast(2) -} - -val Char.isChineseChar: Boolean - get() { - return this.toString().isChineseChar - } - -val String.isChineseChar: Boolean - get() { - return this.matches(Regex("[\u4e00-\u9fa5]")) - } - - -object MiraiConsoleTerminalFrontEnd : MiraiConsoleFrontEnd { - const val cacheLogSize = 50 - var mainTitle = "Mirai Console v0.01 Core v0.15" - - override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) { - mainTitle = "Mirai Console(Terminal) $consoleVersion $consoleBuild Core $coreVersion" - } - - override fun pushLog(identity: Long, message: String) { - log[identity]!!.push(message) - if (identity == screens[currentScreenId]) { - drawLog(message) - } - } - - // 修改interface之后用来暂时占位 - override fun pushLog(priority: LogPriority, identityStr: String, identity: Long, message: String) { - this.pushLog(identity, message) - } - - override fun prePushBot(identity: Long) { - log[identity] = LimitLinkedQueue(cacheLogSize) - } - - override fun pushBot(bot: Bot) { - botAdminCount[bot.id] = 0 - screens.add(bot.id) - drawFrame(this.getScreenName(currentScreenId)) - if (terminal is SwingTerminalFrame) { - terminal.flush() - } - } - - @Volatile - var requesting = false - private var requestResult: String? = null - override suspend fun requestInput(hint:String): String { - if(hint.isNotEmpty()){ - this.pushLog(0, hint) - } - requesting = true - while (requesting) { - delay(100) - } - return requestResult!! - } - - - private fun provideInput(input: String) { - if (requesting) { - requestResult = input - requesting = false - } else { - CommandManager.runCommand(ConsoleCommandSender, commandBuilder.toString()) - } - } - - - override fun pushBotAdminStatus(identity: Long, admins: List) { - botAdminCount[identity] = admins.size - } - - override fun createLoginSolver(): LoginSolver { - return object : LoginSolver() { - override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? { - val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() } - withContext(Dispatchers.IO) { - tempFile.createNewFile() - pushLog(0, "[Login Solver]需要图片验证码登录, 验证码为 4 字母") - try { - tempFile.writeChannel().apply { - writeFully(ByteBuffer.wrap(data)) - close() - } - pushLog(0, "请查看文件 ${tempFile.absolutePath}") - } catch (e: Exception) { - error("[Login Solver]验证码无法保存[Error0001]") - } - } - - lateinit var toLog: String - tempFile.inputStream().use { - val img = ImageIO.read(it) - toLog += img?.createCharImg((terminal.terminalSize.columns / 1.5).toInt()) ?: "无法创建字符图片. 请查看文件\n" - } - return requestInput("$toLog[Login Solver]请输验证码. ${tempFile.absolutePath}") - .takeUnless { it.isEmpty() || it.length != 4 } - .also { - pushLog(0, "[Login Solver]正在提交[$it]中...") - } - } - - override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? { - pushLog(0, "[Login Solver]需要滑动验证码") - pushLog(0, "[Login Solver]请在任意浏览器中打开以下链接并完成验证码. ") - pushLog(0, url) - return requestInput("[Login Solver]完成后请输入任意字符 ").also { - pushLog(0, "[Login Solver]正在提交中") - } - } - - override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? { - pushLog(0, "[Login Solver]需要进行账户安全认证") - pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题") - pushLog(0, "[Login Solver]完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次") - pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符") - pushLog(0, "[Login Solver]这步操作将在后续的版本中优化") - pushLog(0, url) - return requestInput("").also { - pushLog(0, "[Login Solver]正在提交中...") - } - } - - } - } - - private val log = ConcurrentHashMap>().also { - it[0L] = LimitLinkedQueue(cacheLogSize) - } - - private val botAdminCount = ConcurrentHashMap() - - private val screens = mutableListOf(0L) - private var currentScreenId = 0 - - - lateinit var terminal: Terminal - lateinit var textGraphics: TextGraphics - - private var hasStart = false - private lateinit var internalPrinter: PrintStream - fun start() { - if (hasStart) { - return - } - - internalPrinter = System.out - - - hasStart = true - val defaultTerminalFactory = DefaultTerminalFactory(internalPrinter, System.`in`, Charset.defaultCharset()) - try { - terminal = defaultTerminalFactory.createTerminal() - terminal.enterPrivateMode() - terminal.clearScreen() - terminal.setCursorVisible(false) - } catch (e: Exception) { - try { - terminal = SwingTerminalFrame("Mirai Console") - terminal.enterPrivateMode() - terminal.clearScreen() - terminal.setCursorVisible(false) - } catch (e: Exception) { - error("can not create terminal") - } - } - textGraphics = terminal.newTextGraphics() - - /* - var lastRedrawTime = 0L - var lastNewWidth = 0 - var lastNewHeight = 0 - - terminal.addResizeListener(TerminalResizeListener { terminal1: Terminal, newSize: TerminalSize -> - try { - if (lastNewHeight == newSize.rows - && - lastNewWidth == newSize.columns - ) { - return@TerminalResizeListener - } - lastNewHeight = newSize.rows - lastNewWidth = newSize.columns - terminal.clearScreen() - if(terminal !is SwingTerminalFrame) { - Thread.sleep(300) - } - update() - redrawCommand() - redrawLogs(log[screens[currentScreenId]]!!) - }catch (ignored:Exception){ - - } - }) - - */ - var lastJob: Job? = null - terminal.addResizeListener { _: Terminal, _: TerminalSize -> - lastJob = GlobalScope.launch { - try { - delay(300) - if (lastJob == coroutineContext[Job]) { - @Suppress("BlockingMethodInNonBlockingContext") - terminal.clearScreen() - //inited = false - update() - redrawCommand() - redrawLogs(log[screens[currentScreenId]]!!) - } - } catch (e: Exception) { - pushLog(0, "[UI ERROR] ${e.message}") - } - } - } - - if (terminal !is SwingTerminalFrame) { - System.setOut(PrintStream(object : OutputStream() { - var builder = java.lang.StringBuilder() - override fun write(b: Int) { - with(b.toChar()) { - if (this == '\n') { - pushLog(0, builder.toString()) - builder = java.lang.StringBuilder() - } else { - builder.append(this) - } - } - } - })) - } - - System.setErr(System.out) - - try { - update() - } catch (e: Exception) { - pushLog(0, "[UI ERROR] ${e.message}") - } - - val charList = listOf(',', '.', '/', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '=', '+', '!', ' ') - thread { - while (true) { - try { - val keyStroke: KeyStroke = terminal.readInput() - - when (keyStroke.keyType) { - KeyType.ArrowLeft -> { - currentScreenId = - getLeftScreenId() - clearRows(2) - cleanPage() - update() - } - KeyType.ArrowRight -> { - currentScreenId = - getRightScreenId() - clearRows(2) - cleanPage() - update() - } - KeyType.Enter -> { - provideInput(commandBuilder.toString()) - emptyCommand() - } - KeyType.Escape -> { - exit() - } - else -> { - if (keyStroke.character != null) { - if (keyStroke.character.toInt() == 8) { - deleteCommandChar() - } - if (keyStroke.character.isLetterOrDigit() || charList.contains(keyStroke.character)) { - addCommandChar(keyStroke.character) - } - } - } - } - } catch (e: Exception) { - pushLog(0, "[UI ERROR] ${e.message}") - } - } - } - } - - private fun getLeftScreenId(): Int { - var newId = currentScreenId - 1 - if (newId < 0) { - newId = screens.size - 1 - } - return newId - } - - private fun getRightScreenId(): Int { - var newId = 1 + currentScreenId - if (newId >= screens.size) { - newId = 0 - } - return newId - } - - private fun getScreenName(id: Int): String { - return when (screens[id]) { - 0L -> { - "Console Screen" - } - else -> { - "Bot: ${screens[id]}" - } - } - } - - - fun clearRows(row: Int) { - textGraphics.putString( - 0, row, " ".repeat( - terminal.terminalSize.columns - ) - ) - } - - fun drawFrame( - title: String - ) { - val width = terminal.terminalSize.columns - val height = terminal.terminalSize.rows - terminal.setBackgroundColor(TextColor.ANSI.DEFAULT) - - textGraphics.foregroundColor = TextColor.ANSI.WHITE - textGraphics.backgroundColor = TextColor.ANSI.GREEN - textGraphics.putString((width - mainTitle.actualLength) / 2, 1, mainTitle, SGR.BOLD) - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - textGraphics.putString(2, 3, "-".repeat(width - 4)) - textGraphics.putString(2, 5, "-".repeat(width - 4)) - textGraphics.putString(2, height - 4, "-".repeat(width - 4)) - textGraphics.putString(2, height - 3, "|>>>") - textGraphics.putString(width - 3, height - 3, "|") - textGraphics.putString(2, height - 2, "-".repeat(width - 4)) - - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - val leftName = - getScreenName(getLeftScreenId()) - // clearRows(2) - textGraphics.putString((width - title.actualLength) / 2 - "$leftName << ".length, 2, "$leftName << ") - textGraphics.foregroundColor = TextColor.ANSI.WHITE - textGraphics.backgroundColor = TextColor.ANSI.YELLOW - textGraphics.putString((width - title.actualLength) / 2, 2, title, SGR.BOLD) - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - val rightName = - getScreenName(getRightScreenId()) - textGraphics.putString((width + title.actualLength) / 2 + 1, 2, ">> $rightName") - } - - fun drawMainFrame( - onlineBotCount: Number - ) { - drawFrame("Console Screen") - val width = terminal.terminalSize.columns - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - clearRows(4) - textGraphics.putString(2, 4, "|Online Bots: $onlineBotCount") - textGraphics.putString( - width - 2 - "Powered By Mamoe Technologies|".actualLength, - 4, - "Powered By Mamoe Technologies|" - ) - } - - fun drawBotFrame( - qq: Long, - adminCount: Number - ) { - drawFrame("Bot: $qq") - val width = terminal.terminalSize.columns - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - clearRows(4) - textGraphics.putString(2, 4, "|Admins: $adminCount") - textGraphics.putString(width - 2 - "Add admins via commands|".actualLength, 4, "Add admins via commands|") - } - - - object LoggerDrawer { - var currentHeight = 6 - - fun drawLog(string: String, flush: Boolean = true) { - val maxHeight = terminal.terminalSize.rows - 4 - val heightNeed = (string.actualLength / (terminal.terminalSize.columns - 6)) + 1 - if (heightNeed - 1 > maxHeight) { - pushLog(0, "[UI ERROR]: 您的屏幕太小, 有一条超长LOG无法显示") - return//拒绝打印 - } - if (currentHeight + heightNeed > maxHeight) { - cleanPage()//翻页 - } - if (string.contains("\n")) { - string.split("\n").forEach { _ -> - drawLog(string, false) - } - } else { - val width = terminal.terminalSize.columns - 6 - var x = string - while (true) { - if (x == "") { - break - } - val toWrite = if (x.actualLength > width) { - val index = x.getSubStringIndexByActualLength(width) - x.substring(0, index).also { - x = if (index < x.length) { - x.substring(index) - } else { - "" - } - } - } else { - x.also { - x = "" - } - } - try { - textGraphics.foregroundColor = TextColor.ANSI.GREEN - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - textGraphics.putString( - 3, - currentHeight, toWrite, SGR.ITALIC - ) - } catch (ignored: Exception) { - // - } - ++currentHeight - } - } - if (flush && terminal is SwingTerminalFrame) { - terminal.flush() - } - } - - - fun cleanPage() { - for (index in 6 until terminal.terminalSize.rows - 4) { - clearRows(index) - } - currentHeight = 6 - } - - - fun redrawLogs(toDraw: Queue) { - //this.cleanPage() - currentHeight = 6 - var logsToDraw = 0 - var vara = 0 - val toPrint = mutableListOf() - toDraw.forEach { - val heightNeed = (it.actualLength / (terminal.terminalSize.columns - 6)) + 1 - vara += heightNeed - if (currentHeight + vara < terminal.terminalSize.rows - 4) { - logsToDraw++ - toPrint.add(it) - } else { - return@forEach - } - } - toPrint.reversed().forEach { - drawLog(it, false) - } - if (terminal is SwingTerminalFrame) { - terminal.flush() - } - } - } - - - private var commandBuilder = StringBuilder() - private fun redrawCommand() { - val height = terminal.terminalSize.rows - val width = terminal.terminalSize.columns - clearRows(height - 3) - textGraphics.foregroundColor = TextColor.ANSI.DEFAULT - textGraphics.putString(2, height - 3, "|>>>") - textGraphics.putString(width - 3, height - 3, "|") - textGraphics.foregroundColor = TextColor.ANSI.WHITE - textGraphics.backgroundColor = TextColor.ANSI.BLACK - textGraphics.putString(7, height - 3, commandBuilder.toString()) - if (terminal is SwingTerminalFrame) { - terminal.flush() - } - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - } - - private fun addCommandChar( - c: Char - ) { - if (!requesting && commandBuilder.isEmpty() && c != '/') { - addCommandChar('/') - } - textGraphics.foregroundColor = TextColor.ANSI.WHITE - textGraphics.backgroundColor = TextColor.ANSI.BLACK - val height = terminal.terminalSize.rows - commandBuilder.append(c) - if (terminal is SwingTerminalFrame) { - redrawCommand() - } else { - textGraphics.putString(6 + commandBuilder.length, height - 3, c.toString()) - } - textGraphics.backgroundColor = TextColor.ANSI.DEFAULT - } - - private fun deleteCommandChar() { - if (commandBuilder.isNotEmpty()) { - commandBuilder = StringBuilder(commandBuilder.toString().substring(0, commandBuilder.length - 1)) - } - val height = terminal.terminalSize.rows - if (terminal is SwingTerminalFrame) { - redrawCommand() - } else { - textGraphics.putString(7 + commandBuilder.length, height - 3, " ") - } - } - - - private var lastEmpty: Job? = null - private fun emptyCommand() { - commandBuilder = StringBuilder() - if (terminal is SwingTerminal) { - redrawCommand() - terminal.flush() - } else { - lastEmpty = GlobalScope.launch { - try { - delay(100) - if (lastEmpty == coroutineContext[Job]) { - withContext(Dispatchers.IO) { - terminal.clearScreen() - } - //inited = false - update() - redrawCommand() - redrawLogs(log[screens[currentScreenId]]!!) - } - } catch (e: Exception) { - pushLog(0, "[UI ERROR] ${e.message}") - } - } - } - } - - private fun update() { - when (screens[currentScreenId]) { - 0L -> { - drawMainFrame(screens.size - 1) - } - else -> { - drawBotFrame( - screens[currentScreenId], - 0 - ) - } - } - redrawLogs(log[screens[currentScreenId]]!!) - } - - fun exit() { - try { - terminal.exitPrivateMode() - terminal.close() - exitProcess(0) - } catch (ignored: Exception) { - - } - } -} - - -class LimitLinkedQueue( - private val limit: Int = 50 -) : ConcurrentLinkedDeque() { - override fun push(e: T) { - if (size >= limit) { - this.pollLast() - } - return super.push(e) - } -} - -/** - * @author NaturalHG - */ -private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String { - val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt() - val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH) - val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB) - val g2d = image.createGraphics() - g2d.drawImage(tmp, 0, 0, null) - fun gray(rgb: Int): Int { - val r = rgb and 0xff0000 shr 16 - val g = rgb and 0x00ff00 shr 8 - val b = rgb and 0x0000ff - return (r * 30 + g * 59 + b * 11 + 50) / 100 - } - - fun grayCompare(g1: Int, g2: Int): Boolean = - kotlin.math.min(g1, g2).toDouble() / kotlin.math.max(g1, g2) >= ignoreRate - - val background = gray(image.getRGB(0, 0)) - - return buildString(capacity = height) { - - val lines = mutableListOf() - - var minXPos = outputWidth - var maxXPos = 0 - - for (y in 0 until image.height) { - val builderLine = StringBuilder() - for (x in 0 until image.width) { - val gray = gray(image.getRGB(x, y)) - if (grayCompare(gray, background)) { - builderLine.append(" ") - } else { - builderLine.append("#") - if (x < minXPos) { - minXPos = x - } - if (x > maxXPos) { - maxXPos = x - } - } - } - if (builderLine.toString().isBlank()) { - continue - } - lines.add(builderLine) - } - for (line in lines) { - append(line.substring(minXPos, maxXPos)).append("\n") - } - } -} - -// Copied from Ktor CIO -private fun File.writeChannel( - coroutineContext: CoroutineContext = Dispatchers.IO -): ByteWriteChannel = GlobalScope.reader(CoroutineName("file-writer") + coroutineContext, autoFlush = true) { - @Suppress("BlockingMethodInNonBlockingContext") - RandomAccessFile(this@writeChannel, "rw").use { file -> - val copied = channel.copyTo(file.channel) - file.setLength(copied) // truncate tail that could remain from the previously written data - } -}.channel diff --git a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt deleted file mode 100644 index 7cf2e3603..000000000 --- a/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.console - -import net.mamoe.mirai.console.pure.MiraiConsoleUIPure -import kotlin.concurrent.thread - -class MiraiConsoleTerminalLoader { - companion object { - @JvmStatic - fun main(args: Array) { - if (args.contains("pure") || args.contains("-pure") || System.getProperty( - "os.name", - "" - ).toLowerCase().contains("windows") - ) { - println("[MiraiConsoleTerminalLoader]: 将以Pure[兼容模式]启动Console") - MiraiConsole.start(MiraiConsoleUIPure()) - } else { - MiraiConsoleTerminalFrontEnd.start() - thread { - MiraiConsole.start( - MiraiConsoleTerminalFrontEnd - ) - } - } - Runtime.getRuntime().addShutdownHook(thread(start = false) { - MiraiConsole.stop() - MiraiConsoleTerminalFrontEnd.exit() - }) - } - } -} \ No newline at end of file