diff --git a/backend/mirai-console/build.gradle.kts b/backend/mirai-console/build.gradle.kts index ff39265f8..5f275a7f8 100644 --- a/backend/mirai-console/build.gradle.kts +++ b/backend/mirai-console/build.gradle.kts @@ -54,6 +54,7 @@ kotlin { useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") useExperimentalAnnotation("net.mamoe.mirai.console.data.ExperimentalPluginConfig") + useExperimentalAnnotation("net.mamoe.mirai.console.util.ConsoleInternalAPI") } } } 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 618f37e43..b03dbd6c3 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 @@ -21,9 +21,9 @@ import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription 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.CommandManagerImpl import net.mamoe.mirai.console.command.ConsoleCommandSender import net.mamoe.mirai.console.data.PluginDataStorage +import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.plugin.CuiPluginCenter import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl import net.mamoe.mirai.console.internal.util.ConsoleBuiltInPluginDataStorage 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 new file mode 100644 index 000000000..08177c1a7 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/command/CommandPermissionImpl.kt @@ -0,0 +1,23 @@ +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/data/AutoSavePluginData.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/AutoSavePluginData.kt index 8037b167a..33cd0f490 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/AutoSavePluginData.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/AutoSavePluginData.kt @@ -1,10 +1,7 @@ package net.mamoe.mirai.console.internal.data import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import net.mamoe.mirai.console.data.* import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.util.ConsoleInternalAPI @@ -27,6 +24,15 @@ internal open class AutoSavePluginData( override fun setStorage(storage: PluginDataStorage) { check(!this::storage.isInitialized) { "storage is already initialized" } this.storage = storage + + if (shouldPerformAutoSaveWheneverChanged()) { + owner.launch { + while (isActive) { + delay(owner.autoSaveIntervalMillis.last) // 定时自动保存一次, 用于 kts 序列化的对象 + doSave() + } + } + } } @JvmField @@ -34,25 +40,30 @@ internal open class AutoSavePluginData( internal var lastAutoSaveJob: Job? = null @JvmField - @Volatile - internal var currentFirstStartTime = atomic(0L) + internal val currentFirstStartTime = atomic(0L) + + protected open fun shouldPerformAutoSaveWheneverChanged(): Boolean { + return true + } init { owner.coroutineContext[Job]?.invokeOnCompletion { doSave() } } private val updaterBlock: suspend CoroutineScope.() -> Unit = { - currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis }) + if (::storage.isInitialized) { + currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis }) - delay(owner.autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety + delay(owner.autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety - if (lastAutoSaveJob == this.coroutineContext[Job]) { - doSave() - } else { - if (currentFirstStartTime.updateWhen( - { currentTimeMillis - it >= owner.autoSaveIntervalMillis.last }, - { 0 }) - ) doSave() + if (lastAutoSaveJob == this.coroutineContext[Job]) { + doSave() + } else { + if (currentFirstStartTime.updateWhen( + { currentTimeMillis - it >= owner.autoSaveIntervalMillis.last }, + { 0 }) + ) doSave() + } } } 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 f732cfa0c..8a492fce6 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 @@ -52,7 +52,7 @@ internal open class MultiFilePluginDataStorageImpl( }.also { it.setStorage(this) } protected open fun getPluginDataFile(holder: PluginDataHolder, clazz: KClass<*>): File = with(clazz) { - val name = findASerialName() + val name = findValueName() val dir = directoryPath.resolve(holder.name) if (dir.isFile) { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt index 89527e335..2a6982555 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/PluginDataImpl.kt @@ -29,7 +29,7 @@ import net.mamoe.yamlkt.YamlNullableDynamicSerializer * - Auto-saving */ internal abstract class PluginDataImpl { - internal fun findNodeInstance(name: String): ValueNode<*>? = valueNodes.firstOrNull { it.serialName == name } + internal fun findNodeInstance(name: String): ValueNode<*>? = valueNodes.firstOrNull { it.valueName == name } internal abstract val valueNodes: MutableList> @@ -43,8 +43,8 @@ internal abstract class PluginDataImpl { if (decodeSequentially()) { var index = 0 repeat(decodeCollectionSize(descriptor)) { - val serialName = decodeSerializableElement(descriptor, index++, String.serializer()) - val node = findNodeInstance(serialName) + val valueName = decodeSerializableElement(descriptor, index++, String.serializer()) + val node = findNodeInstance(valueName) if (node == null) { decodeSerializableElement(descriptor, index++, YamlNullableDynamicSerializer) } else { @@ -53,21 +53,21 @@ internal abstract class PluginDataImpl { } } else { outerLoop@ while (true) { - var serialName: String? = null + var valueName: String? = null innerLoop@ while (true) { val index = decodeElementIndex(descriptor) if (index == CompositeDecoder.DECODE_DONE) { - check(serialName == null) { "name must be null at this moment." } + check(valueName == null) { "name must be null at this moment." } break@outerLoop } if (!index.isOdd()) { // key - check(serialName == null) { "name must be null at this moment" } - serialName = decodeSerializableElement(descriptor, index, String.serializer()) + check(valueName == null) { "name must be null at this moment" } + valueName = decodeSerializableElement(descriptor, index, String.serializer()) } else { - check(serialName != null) { "name must not be null at this moment" } + check(valueName != null) { "name must not be null at this moment" } - val node = findNodeInstance(serialName) + val node = findNodeInstance(valueName) if (node == null) { decodeSerializableElement(descriptor, index, YamlNullableDynamicSerializer) } else { @@ -92,8 +92,8 @@ internal abstract class PluginDataImpl { var index = 0 // val vSerializer = dataUpdaterSerializerTypeArguments[1] as KSerializer - valueNodes.forEach { (serialName, _, valueSerializer) -> - encodeSerializableElement(descriptor, index++, String.serializer(), serialName) + valueNodes.forEach { (valueName, _, valueSerializer) -> + encodeSerializableElement(descriptor, index++, String.serializer(), valueName) encodeSerializableElement(descriptor, index++, valueSerializer, Unit) } endStructure(descriptor) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt index a108009f0..1f2b4d307 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/data/reflectionUtils.kt @@ -9,8 +9,8 @@ package net.mamoe.mirai.console.internal.data -import kotlinx.serialization.SerialName import net.mamoe.mirai.console.data.PluginData +import net.mamoe.mirai.console.data.ValueName import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import kotlin.reflect.KClass import kotlin.reflect.KParameter @@ -20,7 +20,7 @@ import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.isSubclassOf @Suppress("UNCHECKED_CAST") -internal inline fun KType.asKClass(): KClass { +internal inline fun KType.toKClass(): KClass { val clazz = requireNotNull(classifier as? KClass) { "Unsupported classifier: $classifier" } val fromClass = arguments[0].type?.classifier as? KClass<*> ?: Any::class @@ -34,24 +34,22 @@ internal inline fun KType.asKClass(): KClass { } internal inline fun newPluginDataInstanceUsingReflection(type: KType): T { - val classifier = type.asKClass() + val classifier = type.toKClass() return with(classifier) { objectInstance ?: createInstanceOrNull() ?: throw IllegalArgumentException( "Cannot create PluginData instance. " + - "PluginDataHolder supports PluginDatas implemented as an object " + + "PluginDataHolder supports PluginData implemented as an object " + "or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newPluginDataInstance implementation." ) } } -internal fun isReferenceArray(rootClass: KClass): Boolean = rootClass.java.isArray - @Suppress("UNCHECKED_CAST") -internal fun KType.kclass() = when (val t = classifier) { +internal fun KType.classifierAsKClass() = when (val t = classifier) { is KClass<*> -> t else -> error("Only KClass supported as classifier, got $t") } as KClass @@ -65,14 +63,12 @@ internal fun KClass.createInstanceOrNull(): T? { } @JvmSynthetic -internal fun KClass<*>.findASerialName(): String = - findAnnotation()?.value +internal fun KClass<*>.findValueName(): String = + findAnnotation()?.value ?: qualifiedName ?: throw IllegalArgumentException("Cannot find a serial name for $this") -internal val KProperty<*>.serialNameOrPropertyName: String get() = this.findAnnotation()?.value ?: this.name - internal fun Int.isOdd() = this and 0b1 != 0 -internal val KProperty<*>.serialName: String get() = this.findAnnotation()?.value ?: this.name +internal val KProperty<*>.valueName: String get() = this.findAnnotation()?.value ?: this.name 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 d2ad4352c..2f24fb448 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 @@ -39,7 +39,7 @@ import kotlin.reflect.KType ) internal fun serializerMirai(type: KType): KSerializer { fun serializerByKTypeImpl(type: KType): KSerializer { - val rootClass = type.kclass() + val rootClass = type.classifierAsKClass() val typeArguments = type.arguments .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } @@ -60,7 +60,7 @@ internal fun serializerMirai(type: KType): KSerializer { Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2]) /* mamoe modify */ Any::class -> if (type.isMarkedNullable) YamlNullableDynamicSerializer else YamlDynamicSerializer else -> { - if (isReferenceArray(rootClass)) { + if (rootClass.java.isArray) { return ArraySerializer( typeArguments[0].classifier as KClass, serializers[0] diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description.kt index bb7e42d10..b418f383f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description.kt @@ -87,7 +87,19 @@ public enum class PluginKind { } /** - * 插件的一个依赖的信息 + * 插件的一个依赖的信息. + * + * 在 YAML 格式下, 典型的插件依赖示例: + * ```yaml + * dependencies: + * - name: "依赖的插件名" # 依赖的插件名 + * version: "" # 依赖的版本号, 支持 Apache Ivy 格式. 为 null 或不指定时不限制版本 + * isOptional: true # `true` 表示插件在找不到此依赖时也能正常加载 + * - "SamplePlugin" # 名称为 SamplePlugin 的插件, 不限制版本, isOptional=false + * - "TestPlugin:1.0.0+" # 名称为 ExamplePlugin 的插件, 版本至少为 1.0.0, isOptional=false + * - "ExamplePlugin:1.5.0+?" # 名称为 ExamplePlugin 的插件, 版本至少为 1.5.0, 末尾 `?` 表示 isOptional=true + * - "Another test plugin:[1.0.0, 2.0.0)" # 名称为 Another test plugin 的插件, 版本要求大于等于 1.0.0, 小于 2.0.0, isOptional=false + * ``` * * @see PluginDescription.dependencies */ @@ -100,7 +112,7 @@ public data class PluginDependency( * * 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/), * - * 允许 [Apache Ivy 格式版本号](http://ant.apache.org/ivy/history/latest-milestone/ivyfile/dependency.html) + * 允许 [Apache Ivy 风格版本号表示](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html) */ public val version: @Serializable(SemverAsStringSerializerIvy::class) Semver? = null, /** @@ -124,7 +136,24 @@ public data class PluginDependency( serializer(), Yaml.nonStrict.encodeToString>(any) ) - else -> PluginDependency(any.toString()) + else -> { + var value = any.toString() + val isOptional = value.endsWith('?') + if (isOptional) { + value = value.removeSuffix("?") + } + + val components = value.split(':') + when (components.size) { + 1 -> PluginDependency(value, isOptional = isOptional) + 2 -> PluginDependency( + components[0], + Semver(components[1], Semver.SemverType.IVY), + isOptional = isOptional + ) + else -> error("Illegal plugin dependency statement: $value") + } + } } } ) 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 23f3918c9..82935f904 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 @@ -25,6 +25,7 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllComm import net.mamoe.mirai.console.command.description.CommandArgumentParser import net.mamoe.mirai.console.command.description.buildCommandArgumentContext import net.mamoe.mirai.console.initTestEnvironment +import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.command.flattenCommandComponents import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.PlainText