diff --git a/.github/workflows/cui.yml b/.github/workflows/cui.yml index 0bbcd36eb..eee6c4ecb 100644 --- a/.github/workflows/cui.yml +++ b/.github/workflows/cui.yml @@ -28,7 +28,7 @@ jobs: run: ./gradlew build # if test's failed, don't publish - name: Gradle :mirai-console:cuiCloudUpload run: ./gradlew :mirai-console:cuiCloudUpload -Dcui_cloud_key=${{ secrets.CUI_CLOUD_KEY }} -Pcui_cloud_key=${{ secrets.CUI_CLOUD_KEY }} -Dcui_cloud_url=${{ secrets.CUI_CLOUD_URL }} -Pcui_cloud_url=${{ secrets.CUI_CLOUD_URL }} - - name: Gradle :mirai-console-qqandroid:cuiCloudUpload + - name: Gradle :mirai-console-graphical:cuiCloudUpload run: ./gradlew :mirai-console-graphical:cuiCloudUpload -Dcui_cloud_key=${{ secrets.CUI_CLOUD_KEY }} -Pcui_cloud_key=${{ secrets.CUI_CLOUD_KEY }} -Dcui_cloud_url=${{ secrets.CUI_CLOUD_URL }} -Pcui_cloud_url=${{ secrets.CUI_CLOUD_URL }} diff --git a/.github/workflows/shadow.yml b/.github/workflows/shadow.yml index 8dbaf88c1..2277d04cd 100644 --- a/.github/workflows/shadow.yml +++ b/.github/workflows/shadow.yml @@ -28,7 +28,7 @@ jobs: run: ./gradlew build # if test's failed, don't publish - name: Gradle :mirai-console:githubUpload run: ./gradlew :mirai-console:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} - - name: Gradle :mirai-console-qqandroid:githubUpload + - name: Gradle :mirai-console-graphical:githubUpload run: ./gradlew :mirai-console-graphical:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} diff --git a/.gitignore b/.gitignore index 0e3b5b181..1d533f749 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ keys.properties /plugins/ bintray.user.txt -bintray.key.txt \ No newline at end of file +bintray.key.txt + +token.txt +/*/token.txt \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..aa60a031d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "frontend/mirai-android"] + path = frontend/mirai-android + url = https://github.com/mzdluo123/MiraiAndroid diff --git a/PluginDocs/java/source.java b/PluginDocs/java/source.java index 92ff7d906..d60cf7367 100644 --- a/PluginDocs/java/source.java +++ b/PluginDocs/java/source.java @@ -3,10 +3,10 @@ package net.mamoe.n; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import net.mamoe.mirai.console.command.*; -import net.mamoe.mirai.console.plugins.Config; -import net.mamoe.mirai.console.plugins.ConfigSection; -import net.mamoe.mirai.console.plugins.ConfigSectionFactory; -import net.mamoe.mirai.console.plugins.PluginBase; +import net.mamoe.mirai.console.plugin.Config; +import net.mamoe.mirai.console.plugin.ConfigSection; +import net.mamoe.mirai.console.plugin.ConfigSectionFactory; +import net.mamoe.mirai.console.plugin.PluginBase; import net.mamoe.mirai.console.utils.Utils; import net.mamoe.mirai.message.GroupMessage; import org.jetbrains.annotations.NotNull; diff --git a/mirai-console/README.MD b/backend/codegen/README.MD similarity index 100% rename from mirai-console/README.MD rename to backend/codegen/README.MD diff --git a/backend/codegen/build.gradle.kts b/backend/codegen/build.gradle.kts new file mode 100644 index 000000000..3b254c046 --- /dev/null +++ b/backend/codegen/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("jvm") version "1.4-M2" + kotlin("plugin.serialization") version "1.4-M2" + id("java") +} + +kotlin { + sourceSets { + all { + languageSettings.useExperimentalAnnotation("kotlin.Experimental") + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + languageSettings.progressiveMode = true + languageSettings.languageVersion = "1.4" + languageSettings.apiVersion = "1.4" + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + } + } +} + +dependencies { + api(kotlin("stdlib-jdk8")) + implementation(kotlin("reflect")) +} \ No newline at end of file diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/Codegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/Codegen.kt new file mode 100644 index 000000000..7c7a9b8d4 --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/Codegen.kt @@ -0,0 +1,111 @@ +/* + * 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("FunctionName", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen + +import org.intellij.lang.annotations.Language + +abstract class Replacer(private val name: String) : (String) -> String { + override fun toString(): String { + return name + } +} + +fun Codegen.Replacer(block: (String) -> String): Replacer { + return object : Replacer(this@Replacer::class.simpleName ?: "") { + override fun invoke(p1: String): String = block(p1) + } +} + +class CodegenScope : MutableList by mutableListOf() { + fun applyTo(fileContent: String): String { + return this.fold(fileContent) { acc, replacer -> replacer(acc) } + } + + @CodegenDsl + operator fun Codegen.invoke(vararg ktTypes: KtType) { + if (ktTypes.isEmpty() && this is DefaultInvoke) { + invoke(defaultInvokeArgs) + } + invoke(ktTypes.toList()) + } + + @CodegenDsl + operator fun Codegen.invoke(ktTypes: Collection) { + add(Replacer { + it + buildString { + ktTypes.forEach { applyTo(this, it) } + } + }) + } + + @RegionCodegenDsl + operator fun RegionCodegen.invoke(vararg ktTypes: KtType) = invoke(ktTypes.toList()) + + @RegionCodegenDsl + operator fun RegionCodegen.invoke(ktTypes: Collection) { + add(Replacer { content -> + content.replace(Regex("""//// region $regionName CODEGEN ////([\s\S]*?)( *)//// endregion $regionName CODEGEN ////""")) { result -> + val indent = result.groups[2]!!.value + val indentedCode = CodegenScope() + .apply { (this@invoke as Codegen).invoke(*ktTypes.toTypedArray()) } // add codegen task + .applyTo("") // perform codegen + .lines().dropLastWhile(String::isBlank).joinToString("\n") // remove blank following lines + .mapLine { "${indent}$it" } // indent + """ + |//// region $regionName CODEGEN //// + | + |${indentedCode} + | + |${indent}//// endregion $regionName CODEGEN //// + """.trimMargin() + } + }) + } + + @DslMarker + annotation class CodegenDsl +} + +internal fun String.mapLine(mapper: (String) -> CharSequence) = this.lines().joinToString("\n", transform = mapper) + +@DslMarker +annotation class RegionCodegenDsl + +interface DefaultInvoke { + val defaultInvokeArgs: List +} + +abstract class Codegen { + fun applyTo(stringBuilder: StringBuilder, ktType: KtType) = this.run { stringBuilder.apply(ktType) } + + protected abstract fun StringBuilder.apply(ktType: KtType) +} + +abstract class RegionCodegen(private val targetFile: String, regionName: String? = null) : Codegen() { + val regionName: String by lazy { + regionName ?: this::class.simpleName!!.substringBefore("Codegen") + } + + fun startIndependently() { + codegen(targetFile) { + this@RegionCodegen.invoke() + } + } +} + +abstract class PrimitiveCodegen : Codegen() { + protected abstract fun StringBuilder.apply(ktType: KtPrimitive) + + fun StringBuilder.apply(ktType: List) = ktType.forEach { apply(it) } +} + +fun StringBuilder.appendKCode(@Language("kt") ktCode: String): StringBuilder = append(kCode(ktCode)).appendLine() diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValueSettingCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValueSettingCodegen.kt new file mode 100644 index 000000000..440d0b18b --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/ValueSettingCodegen.kt @@ -0,0 +1,172 @@ +/* + * 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("PRE_RELEASE_CLASS", "ClassName", "RedundantVisibilityModifier") + +package net.mamoe.mirai.console.codegen + +import kotlin.reflect.full.functions +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.isSubclassOf + +internal object ValueSettingCodegen { + /** + * The interface + */ + object PrimitiveValuesCodegen : RegionCodegen("Value.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + @Suppress("ClassName") + appendKCode( + """ + /** + * Represents a non-null [$ktType] value. + */ + public interface ${ktType}Value : PrimitiveValue<$ktType> + """ + ) + } + } + + object BuiltInSerializerConstantsPrimitivesCodegen : RegionCodegen("_Setting.value.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + appendLine( + kCode( + """ + @JvmStatic + internal val ${ktType.standardName}SerializerDescriptor = ${ktType.standardName}.serializer().descriptor + """ + ).lines().joinToString("\n") { " $it" } + ) + } + } + + object PrimitiveValuesImplCodegen : RegionCodegen("_PrimitiveValueDeclarations.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + appendKCode( + """ +internal abstract class ${ktType.standardName}ValueImpl : ${ktType.standardName}Value, SerializerAwareValue<${ktType.standardName}>, KSerializer, AbstractValueImpl<${ktType.standardName}> { + constructor() + constructor(default: ${ktType.standardName}) { + _value = default + } + + private var _value: ${ktType.standardName}? = null + + final override var value: ${ktType.standardName} + get() = _value ?: error("${ktType.standardName}Value.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.${ktType.standardName}SerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = ${ktType.standardName}.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(${ktType.standardName}.serializer().deserialize(decoder)) + override fun toString(): String = _value${if (ktType != KtString) "?.toString()" else ""} ?: "${ktType.standardName}Value.value not yet initialized." + override fun equals(other: Any?): Boolean = other is ${ktType.standardName}ValueImpl && other::class.java == this::class.java && other._value == this._value + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + """ + ) + } + + } + + object Setting_value_PrimitivesImplCodegen : RegionCodegen("_Setting.value.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + appendKCode( + """ +internal fun Setting.valueImpl(default: ${ktType.standardName}): SerializerAwareValue<${ktType.standardName}> { + return object : ${ktType.standardName}ValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.${ktType.lowerCaseName}ValueImpl(): SerializerAwareValue<${ktType.standardName}> { + return object : ${ktType.standardName}ValueImpl() { + override fun onChanged() = this@${ktType.lowerCaseName}ValueImpl.onValueChanged(this) + } +} + """ + ) + } + } + + object Setting_valueImplPrimitiveCodegen : RegionCodegen("_Setting.value.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + appendKCode( + """ + ${ktType.standardName}::class -> ${ktType.lowerCaseName}ValueImpl() + """.trimIndent() + ) + } + } + + object Setting_value_primitivesCodegen : RegionCodegen("Setting.kt"), DefaultInvoke { + @JvmStatic + fun main(args: Array) = super.startIndependently() + override val defaultInvokeArgs: List = KtPrimitives + KtString + + override fun StringBuilder.apply(ktType: KtType) { + @Suppress("unused") + appendKCode( + """ + public fun Setting.value(default: ${ktType.standardName}): SerializerAwareValue<${ktType.standardName}> = valueImpl(default) + """ + ) + } + + } + + /** + * 运行本 object 中所有嵌套 object Codegen + */ + @OptIn(ExperimentalStdlibApi::class) + @JvmStatic + fun main(args: Array) { + ValueSettingCodegen::class.nestedClasses + .filter { it.isSubclassOf(RegionCodegen::class) } + .associateWith { kClass -> kClass.functions.find { it.name == "main" && it.hasAnnotation() } } + .filter { it.value != null } + .forEach { (kClass, entryPoint) -> + println("---------------------------------------------") + println("Running Codegen: ${kClass.simpleName}") + entryPoint!!.call(kClass.objectInstance, arrayOf()) + println("---------------------------------------------") + } + } +} \ No newline at end of file diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/JSettingCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/JSettingCodegen.kt new file mode 100644 index 000000000..495649901 --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/JSettingCodegen.kt @@ -0,0 +1,91 @@ +/* + * 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("PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen.old + +/** + * used to generate Java Setting + */ + + +open class JClazz(val primitiveName: String, val packageName: String) { + open val funName: String = "value" +} + +class JListClazz(val item: JClazz) : JClazz("List<${item.packageName}>", "List<${item.packageName}>") { + override val funName = item.primitiveName.toLowerCase() + "List" +} + +class JArrayClazz(item: JClazz) : JClazz(item.primitiveName + "[]", item.primitiveName + "[]") + +class JMapClazz(key: JClazz, value: JClazz) : + JClazz("Map<${key.packageName},${value.packageName}>", "Map<${key.packageName},${value.packageName}>") + + +internal val J_NUMBERS = listOf( + JClazz("int", "Integer"), + JClazz("short", "Short"), + JClazz("byte", "Byte"), + JClazz("long", "Long"), + JClazz("float", "Float"), + JClazz("double", "Double") +) + +internal val J_EXTRA = listOf( + JClazz("String", "String"), + JClazz("boolean", "Boolean"), + JClazz("char", "Char") +) + + +fun JClazz.getTemplate(): String = """ + @NotNull default Value<${this.packageName}> $funName(${this.primitiveName} defaultValue){ + return _SettingKt.value(this,defaultValue); + } + """ + + +fun main() { + println(buildString { + appendLine(COPYRIGHT) + appendLine() + appendLine(FILE_SUPPRESS) + appendLine() + appendLine( + "/**\n" + + " * !!! This file is auto-generated by backend/codegen/src/kotlin/net.mamoe.mirai.console.codegen.JSettingCodegen.kt\n" + + " * !!! DO NOT MODIFY THIS FILE MANUALLY\n" + + " */\n" + + "\"\"\"" + ) + appendLine() + appendLine() + + + //do simplest + (J_EXTRA + J_NUMBERS).forEach { + appendLine(it.getTemplate()) + } + + (J_EXTRA + J_NUMBERS).forEach { + appendLine(JListClazz(it).getTemplate()) + } + + (J_EXTRA + J_NUMBERS).forEach { + appendLine(JArrayClazz(it).getTemplate()) + } + + (J_EXTRA + J_NUMBERS).forEach { key -> + (J_EXTRA + J_NUMBERS).forEach { value -> + appendLine(JMapClazz(key, value).getTemplate()) + } + } + }) +} \ No newline at end of file diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/SettingValueUseSiteCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/SettingValueUseSiteCodegen.kt new file mode 100644 index 000000000..e81d9a74d --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/SettingValueUseSiteCodegen.kt @@ -0,0 +1,169 @@ +/* + * 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("PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen.old + +import org.intellij.lang.annotations.Language +import java.io.File + + +fun main() { + println(File("").absolutePath) // default project base dir + + File("backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/_Setting.kt").apply { + createNewFile() + }.writeText(buildString { + appendLine(COPYRIGHT) + appendLine() + appendLine(FILE_SUPPRESS) + appendLine() + appendLine(PACKAGE) + appendLine() + appendLine(IMPORTS) + appendLine() + appendLine() + appendLine(DO_NOT_MODIFY) + appendLine() + appendLine() + appendLine(genAllValueUseSite()) + }) +} + +private val DO_NOT_MODIFY = """ +/** + * !!! This file is auto-generated by backend/codegen/src/kotlin/net.mamoe.mirai.console.codegen.SettingValueUseSiteCodegen.kt + * !!! DO NOT MODIFY THIS FILE MANUALLY + */ +""".trimIndent() + +private val PACKAGE = """ +package net.mamoe.mirai.console.setting +""".trimIndent() + +internal val FILE_SUPPRESS = """ +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "unused") +""".trimIndent() +private val IMPORTS = """ +import net.mamoe.mirai.console.setting.internal.valueImpl +import kotlin.internal.LowPriorityInOverloadResolution +""".trimIndent() + +fun genAllValueUseSite(): String = buildString { + fun appendln(@Language("kt") code: String) { + this.appendLine(code.trimIndent()) + } + // PRIMITIVE + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln(genValueUseSite(number, number)) + } + + // PRIMITIVE ARRAYS + for (number in NUMBERS + OTHER_PRIMITIVES.filterNot { it == "String" }) { + appendln( + genValueUseSite( + "${number}Array", + "${number}Array" + ) + ) + } + + // TYPED ARRAYS + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + genValueUseSite( + "Array<${number}>", + "Typed${number}Array" + ) + ) + } + + // PRIMITIVE LISTS / PRIMITIVE SETS + for (collectionName in listOf("List", "Set")) { + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + genValueUseSite( + "${collectionName}<${number}>", + "${number}${collectionName}" + ) + ) + } + } + + // MUTABLE LIST / MUTABLE SET + for (collectionName in listOf("List", "Set")) { + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendLine() + appendln( + """ + @JvmName("valueMutable") + fun Setting.value(default: Mutable${collectionName}<${number}>): Mutable${number}${collectionName}Value = valueImpl(default) + """.trimIndent() + ) + } + } + + // SPECIAL + appendLine() + appendln( + """ + fun Setting.value(default: T): Value { + require(this::class != default::class) { + "Recursive nesting is prohibited" + } + return valueImpl(default).also { + if (default is Setting.NestedSetting) { + default.attachedValue = it + } + } + } + + inline fun Setting.value(default: T, crossinline initializer: T.() -> Unit): Value = + value(default).also { it.value.apply(initializer) } + + inline fun Setting.value(default: List): SettingListValue = valueImpl(default) + + @JvmName("valueMutable") + inline fun Setting.value(default: MutableList): MutableSettingListValue = valueImpl(default) + + + inline fun Setting.value(default: Set): SettingSetValue = valueImpl(default) + + @JvmName("valueMutable") + inline fun Setting.value(default: MutableSet): MutableSettingSetValue = valueImpl(default) + + /** + * 创建一个只引用对象而不跟踪其属性的值. + * + * @param T 类型. 必须拥有 [kotlinx.serialization.Serializable] 注解 (因此编译器会自动生成序列化器) + */ + @DangerousReferenceOnlyValue + @JvmName("valueDynamic") + @LowPriorityInOverloadResolution + inline fun Setting.value(default: T): Value = valueImpl(default) + + @RequiresOptIn( + ""${'"'} + 这种只保存引用的 Value 可能会导致意料之外的结果, 在使用时须保持谨慎. + 对值的改变不会触发自动保存, 也不会同步到 UI 中. 在 UI 中只能编辑序列化之后的值. + ""${'"'}, level = RequiresOptIn.Level.WARNING + ) + @Retention(AnnotationRetention.BINARY) + @Target(AnnotationTarget.FUNCTION) + annotation class DangerousReferenceOnlyValue + """ + ) +} + +fun genValueUseSite(kotlinTypeName: String, miraiValueName: String): String = + """ + fun Setting.value(default: $kotlinTypeName): ${miraiValueName}Value = valueImpl(default) + """.trimIndent() + diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValueImplCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValueImplCodegen.kt new file mode 100644 index 000000000..90114a110 --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValueImplCodegen.kt @@ -0,0 +1,267 @@ +/* + * 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("PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen.old + +import org.intellij.lang.annotations.Language +import java.io.File + + +fun main() { + println(File("").absolutePath) // default project base dir + + File("backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_ValueImpl.kt").apply { + createNewFile() + }.writeText(buildString { + appendLine(COPYRIGHT) + appendLine() + appendLine(PACKAGE) + appendLine() + appendLine(IMPORTS) + appendLine() + appendLine() + appendLine(DO_NOT_MODIFY) + appendLine() + appendLine() + appendLine(genAllValueImpl()) + }) +} + +private val DO_NOT_MODIFY = """ +/** + * !!! This file is auto-generated by backend/codegen/src/kotlin/net.mamoe.mirai.console.codegen.ValueImplCodegen.kt + * !!! DO NOT MODIFY THIS FILE MANUALLY + */ +""".trimIndent() + +private val PACKAGE = """ +package net.mamoe.mirai.console.setting.internal +""".trimIndent() + +private val IMPORTS = """ +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import net.mamoe.mirai.console.setting.* +""".trimIndent() + +fun genAllValueImpl(): String = buildString { + fun appendln(@Language("kt") code: String) { + this.appendLine(code.trimIndent()) + } + + // PRIMITIVE + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + genPrimitiveValueImpl( + number, + number, + "$number.serializer()", + false + ) + ) + appendLine() + } + + // PRIMITIVE ARRAYS + for (number in NUMBERS + OTHER_PRIMITIVES.filterNot { it == "String" }) { + appendln( + genPrimitiveValueImpl( + "${number}Array", + "${number}Array", + "${number}ArraySerializer()", + true + ) + ) + appendLine() + } + + // TYPED ARRAYS + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + genPrimitiveValueImpl( + "Array<${number}>", + "Typed${number}Array", + "ArraySerializer(${number}.serializer())", + true + ) + ) + appendLine() + } + + // PRIMITIVE LISTS / SETS + for (collectionName in listOf("List", "Set")) { + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + genCollectionValueImpl( + collectionName, + "${collectionName}<${number}>", + "${number}${collectionName}", + "${collectionName}Serializer(${number}.serializer())", + false + ) + ) + appendLine() + } + } + + appendLine() + + // MUTABLE LIST / MUTABLE SET + + for (collectionName in listOf("List", "Set")) { + for (number in NUMBERS + OTHER_PRIMITIVES) { + appendln( + """ + @JvmName("valueImplMutable${number}${collectionName}") + internal fun Setting.valueImpl( + default: Mutable${collectionName}<${number}> + ): Mutable${number}${collectionName}Value { + var internalValue: Mutable${collectionName}<${number}> = default + + val delegt = dynamicMutable${collectionName} { internalValue } + return object : Mutable${number}${collectionName}Value(), Mutable${collectionName}<${number}> by delegt { + override var value: Mutable${collectionName}<${number}> + get() = internalValue + set(new) { + if (new != internalValue) { + internalValue = new + onElementChanged(this) + } + } + + private val outerThis get() = this + + override val serializer: KSerializer> = object : KSerializer> { + private val delegate = ${collectionName}Serializer(${number}.serializer()) + override val descriptor: SerialDescriptor get() = delegate.descriptor + + override fun deserialize(decoder: Decoder): Mutable${collectionName}<${number}> { + return delegate.deserialize(decoder).toMutable${collectionName}().observable { + onElementChanged(outerThis) + } + } + + override fun serialize(encoder: Encoder, value: Mutable${collectionName}<${number}>) { + delegate.serialize(encoder, value) + } + } + } + } + """ + ) + appendLine() + } + } + + appendLine() + + + appendln( + """ + internal fun Setting.valueImpl(default: T): Value { + return object : SettingValue() { + private var internalValue: T = default + override var value: T + get() = internalValue + set(new) { + if (new != internalValue) { + internalValue = new + onElementChanged(this) + } + } + override val serializer = object : KSerializer{ + override val descriptor: SerialDescriptor + get() = internalValue.updaterSerializer.descriptor + + override fun deserialize(decoder: Decoder): T { + internalValue.updaterSerializer.deserialize(decoder) + return internalValue + } + + override fun serialize(encoder: Encoder, value: T) { + internalValue.updaterSerializer.serialize(encoder, SettingSerializerMark) + } + } + } + } + """ + ) +} + +fun genPrimitiveValueImpl( + kotlinTypeName: String, + miraiValueName: String, + serializer: String, + isArray: Boolean +): String = + """ + internal fun Setting.valueImpl(default: ${kotlinTypeName}): ${miraiValueName}Value { + return object : ${miraiValueName}Value() { + private var internalValue: $kotlinTypeName = default + override var value: $kotlinTypeName + get() = internalValue + set(new) { + ${ + if (isArray) """ + if (!new.contentEquals(internalValue)) { + internalValue = new + onElementChanged(this) + } + """.trim() + else """ + if (new != internalValue) { + internalValue = new + onElementChanged(this) + } + """.trim() + } + } + override val serializer get() = $serializer + } + } + """.trimIndent() + "\n" + + +fun genCollectionValueImpl( + collectionName: String, + kotlinTypeName: String, + miraiValueName: String, + serializer: String, + isArray: Boolean +): String = + """ + internal fun Setting.valueImpl(default: ${kotlinTypeName}): ${miraiValueName}Value { + var internalValue: $kotlinTypeName = default + val delegt = dynamic$collectionName { internalValue } + return object : ${miraiValueName}Value(), $kotlinTypeName by delegt { + override var value: $kotlinTypeName + get() = internalValue + set(new) { + ${ + if (isArray) """ + if (!new.contentEquals(internalValue)) { + internalValue = new + onElementChanged(this) + } + """.trim() + else """ + if (new != internalValue) { + internalValue = new + onElementChanged(this) + } + """.trim() + } + } + override val serializer get() = $serializer + } + } + """.trimIndent() + "\n" + diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValuesCodegen.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValuesCodegen.kt new file mode 100644 index 000000000..c25da5db8 --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/old/ValuesCodegen.kt @@ -0,0 +1,271 @@ +/* + * 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("ClassName", "unused", "PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen.old + +import org.intellij.lang.annotations.Language +import java.io.File + +fun main() { + println(File("").absolutePath) // default project base dir + + File("backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/_Value.kt").apply { + createNewFile() + }.writeText(genPublicApi()) +} + +internal val COPYRIGHT = """ +/* + * 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 + */ +""".trim() + +internal val NUMBERS = listOf( + "Int", + "Short", + "Byte", + "Long", + "Float", + "Double" +) + +internal val UNSIGNED_NUMBERS = listOf( + "UInt", + "UShort", + "UByte", + "ULong" +) + +internal val OTHER_PRIMITIVES = listOf( + "Boolean", + "Char", + "String" +) + +fun genPublicApi() = buildString { + fun appendln(@Language("kt") code: String) { + this.appendLine(code.trimIndent()) + } + + appendln(COPYRIGHT.trim()) + appendLine() + appendln( + """ + package net.mamoe.mirai.console.setting + + import kotlinx.serialization.KSerializer + import kotlin.properties.ReadWriteProperty + import kotlin.reflect.KProperty + """ + ) + appendLine() + appendln( + """ + /** + * !!! This file is auto-generated by backend/codegen/src/main/kotlin/net.mamoe.mirai.console.codegen.ValuesCodegen.kt + * !!! for better performance + * !!! DO NOT MODIFY THIS FILE MANUALLY + */ + """ + ) + appendLine() + + appendln( + """ +sealed class Value : ReadWriteProperty { + abstract var value: T + + /** + * 用于更新 [value] 的序列化器 + */ + abstract val serializer: KSerializer + override fun getValue(thisRef: Setting, property: KProperty<*>): T = value + override fun setValue(thisRef: Setting, property: KProperty<*>, value: T) { + this.value = value + } + + override fun equals(other: Any?): Boolean { + if (other==null)return false + if (other::class != this::class) return false + other as Value<*> + return other.value == this.value + } + + override fun hashCode(): Int = value.hashCode() +} + """ + ) + appendLine() + + // PRIMITIVES + + appendln( + """ + sealed class PrimitiveValue : Value() + + sealed class NumberValue : Value() + """ + ) + + for (number in NUMBERS) { + val template = """ + abstract class ${number}Value internal constructor() : NumberValue<${number}>() + """ + + appendln(template) + } + + appendLine() + + for (number in OTHER_PRIMITIVES) { + val template = """ + abstract class ${number}Value internal constructor() : PrimitiveValue<${number}>() + """ + + appendln(template) + } + + appendLine() + + // ARRAYS + + appendln( + """ + // T can be primitive array or typed Array + sealed class ArrayValue : Value() + """ + ) + + // PRIMITIVE ARRAYS + appendln( + """ + sealed class PrimitiveArrayValue : ArrayValue() + """ + ) + appendLine() + + for (number in (NUMBERS + OTHER_PRIMITIVES).filterNot { it == "String" }) { + appendln( + """ + abstract class ${number}ArrayValue internal constructor() : PrimitiveArrayValue<${number}Array>(), Iterable<${number}> { + override fun iterator(): Iterator<${number}> = this.value.iterator() + } + """ + ) + appendLine() + } + + appendLine() + + // TYPED ARRAYS + + appendln( + """ + sealed class TypedPrimitiveArrayValue : ArrayValue>() , Iterable{ + override fun iterator() = this.value.iterator() + } + """ + ) + appendLine() + + for (number in (NUMBERS + OTHER_PRIMITIVES)) { + appendln( + """ + abstract class Typed${number}ArrayValue internal constructor() : TypedPrimitiveArrayValue<${number}>() + """ + ) + } + + appendLine() + + // TYPED LISTS / SETS + for (collectionName in listOf("List", "Set")) { + + appendln( + """ + sealed class ${collectionName}Value : Value<${collectionName}>(), ${collectionName} + """ + ) + + for (number in (NUMBERS + OTHER_PRIMITIVES)) { + val template = """ + abstract class ${number}${collectionName}Value internal constructor() : ${collectionName}Value<${number}>() + """ + + appendln(template) + } + + appendLine() + // SETTING + appendln( + """ + abstract class Setting${collectionName}Value internal constructor() : Value<${collectionName}>(), ${collectionName} + """ + ) + appendLine() + } + + // SETTING VALUE + + appendln( + """ + abstract class SettingValue internal constructor() : Value() + """ + ) + + appendLine() + + // MUTABLE LIST / MUTABLE SET + for (collectionName in listOf("List", "Set")) { + appendln( + """ + abstract class Mutable${collectionName}Value internal constructor() : Value>>(), Mutable${collectionName} + """ + ) + + appendLine() + + for (number in (NUMBERS + OTHER_PRIMITIVES)) { + appendln( + """ + abstract class Mutable${number}${collectionName}Value internal constructor() : Value>(), Mutable${collectionName}<${number}> + """ + ) + } + + appendLine() + // SETTING + appendln( + """ + abstract class MutableSetting${collectionName}Value internal constructor() : Value>(), Mutable${collectionName} + """ + ) + appendLine() + } + + appendLine() + // DYNAMIC + + appendln( + """ + /** + * 只引用这个对象, 而不跟踪其成员. + * 仅适用于基础类型, 用于 mutable list/map 等情况; 或标注了 [Serializable] 的类. + */ + abstract class DynamicReferenceValue : Value() + """ + ) +} \ No newline at end of file diff --git a/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/util.kt b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/util.kt new file mode 100644 index 000000000..9ccff0c00 --- /dev/null +++ b/backend/codegen/src/main/kotlin/net/mamoe/mirai/console/codegen/util.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate", "unused","PRE_RELEASE_CLASS") + +package net.mamoe.mirai.console.codegen + +import org.intellij.lang.annotations.Language +import java.io.File + + +typealias KtByte = KtType.KtPrimitive.KtByte +typealias KtShort = KtType.KtPrimitive.KtShort +typealias KtInt = KtType.KtPrimitive.KtInt +typealias KtLong = KtType.KtPrimitive.KtLong +typealias KtFloat = KtType.KtPrimitive.KtFloat +typealias KtDouble = KtType.KtPrimitive.KtDouble +typealias KtChar = KtType.KtPrimitive.KtChar +typealias KtBoolean = KtType.KtPrimitive.KtBoolean + +typealias KtString = KtType.KtString + +typealias KtCollection = KtType.KtCollection +typealias KtMap = KtType.KtMap + +typealias KtPrimitive = KtType.KtPrimitive + + +sealed class KtType { + /** + * Its classname in standard library + */ + abstract val standardName: String + override fun toString(): String = standardName + + /** + * Not Including [String] + */ + sealed class KtPrimitive( + override val standardName: String, + val jPrimitiveName: String = standardName.toLowerCase(), + val jObjectName: String = standardName + ) : KtType() { + object KtByte : KtPrimitive("Byte") + object KtShort : KtPrimitive("Short") + object KtInt : KtPrimitive("Int", jObjectName = "Integer") + object KtLong : KtPrimitive("Long") + + object KtFloat : KtPrimitive("Float") + object KtDouble : KtPrimitive("Double") + + object KtChar : KtPrimitive("Char", jObjectName = "Character") + object KtBoolean : KtPrimitive("Boolean") + } + + object KtString : KtType() { + override val standardName: String get() = "String" + } + + /** + * [List], [Set] + */ + data class KtCollection(override val standardName: String) : KtType() + + object KtMap : KtType() { + override val standardName: String get() = "Map" + } +} + +val KtPrimitiveIntegers = listOf(KtByte, KtShort, KtInt, KtLong) +val KtPrimitiveFloatings = listOf(KtFloat, KtDouble) + +val KtPrimitiveNumbers = KtPrimitiveIntegers + KtPrimitiveFloatings +val KtPrimitiveNonNumbers = listOf(KtChar, KtBoolean) + +val KtPrimitives = KtPrimitiveNumbers + KtPrimitiveNonNumbers + +operator fun KtType.plus(type: KtType): List { + return listOf(this, type) +} + +val KtType.lowerCaseName: String get() = this.standardName.toLowerCase() + +inline fun kCode(@Language("kt") source: String) = source.trimIndent() + +fun codegen(targetFile: String, block: CodegenScope.() -> Unit) { + //// region PrimitiveValue CODEGEN //// + //// region PrimitiveValue CODEGEN //// + + targetFile.findFileSmart().also { + println("Codegen target: ${it.absolutePath}") + }.apply { + writeText( + CodegenScope().apply(block).also { list -> + list.forEach { + println("Applying replacement: $it") + } + }.applyTo(readText()) + ) + } +} + +fun String.findFileSmart(): File = kotlin.run { + if (contains("/")) { // absolute + File(this) + } else { + val list = File(".").walk().filter { it.name == this }.toList() + if (list.isNotEmpty()) return list.single() + + File(".").walk().filter { it.name.contains(this) }.single() + } +}.also { + require(it.exists()) { "file doesn't exist" } +} \ No newline at end of file diff --git a/backend/mirai-console/README.MD b/backend/mirai-console/README.MD new file mode 100644 index 000000000..6d5db89bc --- /dev/null +++ b/backend/mirai-console/README.MD @@ -0,0 +1,87 @@ +# Mirai Console +你可以在全平台运行Mirai高效率机器人框架 +### Mirai Console提供了6个版本以满足各种需要 +#### 所有版本的Mirai Console API相同 插件系统相同 + +| 名字 | 介绍 | +|:------------------------|:------------------------------| +| Mirai-Console-Pure | 最纯净版, CLI环境, 通过标准输入与标准输出 交互 | +| Mirai-Console-Terminal | (UNIX)Terminal环境 提供简洁的富文本控制台 | +| Mirai-Console-Android | 安卓APP (TODO) | +| Mirai-Console-Graphical | JavaFX的图形化界面 (.jar/.exe/.dmg) | +| Mirai-Console-WebPanel | Web Panel操作(TODO) | +| Mirai-Console-Ios | IOS APP (TODO) | + + +### 如何选择版本 +1: Mirai-Console-Pure 兼容性最高, 在其他都表现不佳的时候请使用
+2: 以系统区分 +```kotlin + return when(operatingSystem){ + WINDOWS -> listOf("Graphical","WebPanel","Pure") + MAC_OS -> listOf("Graphical","Terminal","WebPanel","Pure") + LINUX -> listOf("Terminal","Pure") + ANDROID -> listOf("Android","Pure","WebPanel") + IOS -> listOf("Ios") + else -> listOf("Pure") + } +``` +3: 以策略区分 +```kotlin + return when(task){ + 体验 -> listOf("Graphical","Terminal","WebPanel","Android","Pure") + 测试插件 -> listOf("Pure") + 调试插件 -> byOperatingSystem() + 稳定挂机 -> listOf("Terminal","Pure") + else -> listOf("Pure") + } +``` + + +#### More Importantly, Mirai Console support Plugins, tells the bot what to do +#### Mirai Console 支持插件系统, 你可以自己开发或使用公开的插件来逻辑化机器人, 如群管 +
+ +#### download 下载 +#### how to get/write plugins 如何获取/写插件 +
+
+ +### how to use(如何使用) +#### how to run Mirai Console +
    +
  • download mirai-console.jar
  • +
  • open command line/terminal
  • +
  • create a folder and put mirai-console.jar in
  • +
  • cd that folder
  • +
  • "java -jar mirai-console.jar"
  • +
+ +
    +
  • 下载mirai-console.jar
  • +
  • 打开终端
  • +
  • 在任何地方创建一个文件夹, 并放入mirai-console.jar
  • +
  • 在终端中打开该文件夹"cd"
  • +
  • 输入"java -jar mirai-console.jar"
  • +
+ +#### how to add plugins +
    +
  • After first time of running mirai console
  • +
  • /plugins/folder will be created next to mirai-console.jar
  • +
  • put plugin(.jar) into /plugins/
  • +
  • restart mirai console
  • +
  • checking logger and check if the plugin is loaded successfully
  • +
  • if the plugin has it own Config file, it normally appears in /plugins/{pluginName}/
  • +
+ +
    +
  • 在首次运行mirai console后
  • +
  • mirai-console.jar 的同级会出现/plugins/文件夹
  • +
  • 将插件(.jar)放入/plugins/文件夹
  • +
  • 重启mirai console
  • +
  • 在开启后检查日志, 是否成功加载
  • +
  • 如该插件有配置文件, 配置文件一般会创建在/plugins/插件名字/ 文件夹下
  • +
+ + diff --git a/backend/mirai-console/build.gradle.kts b/backend/mirai-console/build.gradle.kts new file mode 100644 index 000000000..1897eb648 --- /dev/null +++ b/backend/mirai-console/build.gradle.kts @@ -0,0 +1,131 @@ +@file:Suppress("UnusedImport") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone + +plugins { + kotlin("jvm") version Versions.kotlinCompiler + kotlin("plugin.serialization") version Versions.kotlinCompiler + id("java") + `maven-publish` + id("com.jfrog.bintray") +} + +version = Versions.console +description = "Console backend for mirai" + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(JavaCompile::class.java) { + options.encoding = "UTF8" +} + +kotlin { + explicitApiWarning() + + sourceSets.all { + target.compilations.all { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=enable" + jvmTarget = "1.8" + // useIR = true + } + } + languageSettings.apply { + enableLanguageFeature("InlineClasses") + progressiveMode = true + + useExperimentalAnnotation("kotlin.Experimental") + useExperimentalAnnotation("kotlin.OptIn") + + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation") + useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI") + useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + } + } +} + +dependencies { + compileAndRuntime("net.mamoe:mirai-core:${Versions.core}") + compileAndRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib)) + + implementation(kotlinx("serialization-runtime", Versions.serialization)) + + implementation("net.mamoe.yamlkt:yamlkt:0.3.1") + api("org.jetbrains:annotations:19.0.0") + api(kotlinx("coroutines-jdk8", Versions.coroutines)) + + api("com.vdurmont:semver4j:3.1.0") + + //api(kotlinx("collections-immutable", Versions.collectionsImmutable)) + + testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") + testApi(kotlin("stdlib-jdk8")) + testApi(kotlin("test")) + testApi(kotlin("test-junit5")) + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") +} + +ext.apply { + // 傻逼 compileAndRuntime 没 exclude 掉 + // 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置 + val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = { + dependencyFilter.exclude { + when ("${it.moduleGroup}:${it.moduleName}") { + "net.mamoe:mirai-core" -> true + "net.mamoe:mirai-core-qqandroid" -> true + else -> false + } + } + } + set("shadowJar", x) +} + +tasks { + "test"(Test::class) { + useJUnitPlatform() + } + + val compileKotlin by getting {} + + val fillBuildConstants by registering { + group = "mirai" + doLast { + (compileKotlin as KotlinCompile).source.filter { it.name == "MiraiConsole.kt" }.single().let { file -> + file.writeText(file.readText() + .replace(Regex("""val buildDate: Date = Date\((.*)\) //(.*)""")) { + """ + val buildDate: Date = Date(${System.currentTimeMillis()}L) // ${ + SimpleDateFormat("yyyy-MM-dd HH:mm:ss").apply { + timeZone = TimeZone.getTimeZone("GMT+8") + }.format(Date()) + } + """.trimIndent() + } + .replace(Regex("""const val version: String = "(.*)"""")) { + """ + const val version: String = "${Versions.console}" + """.trimIndent() + } + ) + } + } + } +} + +// region PUBLISHING + +setupPublishing("mirai-console") + +// endregion \ No newline at end of file diff --git a/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java new file mode 100644 index 000000000..74ad92fa9 --- /dev/null +++ b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java @@ -0,0 +1,186 @@ +package net.mamoe.mirai.console.command; + +import kotlin.NotImplementedError; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.BuildersKt; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.CoroutineStart; +import kotlinx.coroutines.future.FutureKt; +import net.mamoe.mirai.console.plugin.jvm.JavaPlugin; +import net.mamoe.mirai.message.data.Message; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Java 适配的 {@link CommandManagerKt} + */ +@SuppressWarnings({"unused", "RedundantSuppression"}) +public final class JCommandManager { + private JCommandManager() { + throw new NotImplementedError(); + } + + /** + * 获取指令前缀 + * + * @return 指令前缀 + */ + @NotNull + public static String getCommandPrefix() { + return CommandManagerKt.getCommandPrefix(); + } + + /** + * 获取一个指令所有者已经注册了的指令列表. + * + * @param owner 指令所有者 + * @return 指令列表 + */ + @NotNull + public static List<@NotNull Command> getRegisteredCommands(final @NotNull CommandOwner owner) { + return CommandManagerKt.getRegisteredCommands(Objects.requireNonNull(owner, "owner")); + } + + /** + * 注册一个指令. + * + * @param command 指令实例 + * @param override 是否覆盖重名指令. + *

+ * 若原有指令 P, 其 {@link Command#getNames()} 为 'a', 'b', 'c'.
+ * 新指令 Q, 其 {@link Command#getNames()} 为 'b', 将会覆盖原指令 A 注册的 'b'. + *

+ * 即注册完成后, 'a' 和 'c' 将会解析到指令 P, 而 'b' 会解析到指令 Q. + * @return 若已有重名指令, 且 overridefalse, 返回 false;
+ * 若已有重名指令, 但 overridetrue, 覆盖原有指令并返回 true. + */ + public static boolean register(final @NotNull Command command, final boolean override) { + Objects.requireNonNull(command, "command"); + return CommandManagerKt.register(command, override); + } + + /** + * 注册一个指令, 已有重复名称的指令时返回 false + * + * @param command 指令实例 + * @return 若已有重名指令, 返回 false, 否则返回 true. + */ + public static boolean register(final @NotNull Command command) { + Objects.requireNonNull(command, "command"); + return register(command, false); + } + + /** + * 查找并返回重名的指令. 返回重名指令. + */ + @Nullable + public static Command findDuplicate(final @NotNull Command command) { + Objects.requireNonNull(command, "command"); + return CommandManagerKt.findDuplicate(command); + } + + /** + * 取消注册这个指令. 若指令未注册, 返回 false. + */ + public static boolean unregister(final @NotNull Command command) { + Objects.requireNonNull(command, "command"); + return CommandManagerKt.unregister(command); + } + + /** + * 取消注册所有属于 owner 的指令 + * + * @param owner 指令所有者 + */ + public static void unregisterAllCommands(final @NotNull CommandOwner owner) { + Objects.requireNonNull(owner, "owner"); + CommandManagerKt.unregisterAllCommands(owner); + } + + + /** + * 解析并执行一个指令 + * + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 成功执行的指令, 在无匹配指令时返回 null + * @throws CommandExecutionException 当 {@link Command#onCommand(CommandSender, Object[], Continuation)} 抛出异常时包装并附带相关指令信息抛出 + * @see #executeCommandAsync(CoroutineScope, CommandSender, Object...) + */ + @Nullable + public static Command executeCommand(final @NotNull CommandSender sender, final @NotNull Object... args) throws CommandExecutionException, InterruptedException { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, completion) -> CommandManagerKt.executeCommand(sender, args, completion)); + } + + /** + * 异步 (在 Kotlin 协程线程池) 解析并执行一个指令 + * + * @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例. + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 成功执行的指令, 在无匹配指令时返回 null + * @see #executeCommand(CommandSender, Object...) + */ + @NotNull + public static CompletableFuture<@Nullable Command> executeCommandAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + Objects.requireNonNull(scope, "scope"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommand(sender, args, completion)); + } + + + /** + * 解析并执行一个指令, 获取详细的指令参数等信息. + *
+ * 执行过程中产生的异常将不会直接抛出, 而会包装为 {@link CommandExecuteResult.ExecutionException} + * + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 执行结果 + * @see #executeCommandDetailedAsync(CoroutineScope, CommandSender, Object...) + */ + @NotNull + public static CommandExecuteResult executeCommandDetailed(final @NotNull CommandSender sender, final @NotNull Object... args) throws InterruptedException { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion)); + } + + /** + * 异步 (在 Kotlin 协程线程池) 解析并执行一个指令, 获取详细的指令参数等信息 + * + * @param scope 协程作用域 (用于管理协程生命周期). 一般填入 {@link JavaPlugin} 实例. + * @param args 接受 {@link String} 或 {@link Message} , 其他对象将会被 {@link Object#toString()} + * @return 执行结果 + * @see #executeCommandDetailed(CommandSender, Object...) + */ + @NotNull + public static CompletableFuture<@NotNull CommandExecuteResult> + executeCommandDetailedAsync(final @NotNull CoroutineScope scope, final @NotNull CommandSender sender, final @NotNull Object... args) { + Objects.requireNonNull(sender, "sender"); + Objects.requireNonNull(args, "args"); + Objects.requireNonNull(scope, "scope"); + for (Object arg : args) { + Objects.requireNonNull(arg, "element of args"); + } + + return FutureKt.future(scope, EmptyCoroutineContext.INSTANCE, CoroutineStart.DEFAULT, (sc, completion) -> CommandManagerKt.executeCommandDetailed(sender, args, completion)); + } +} diff --git a/backend/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java new file mode 100644 index 000000000..285bcfb48 --- /dev/null +++ b/backend/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java @@ -0,0 +1,39 @@ +/* + * 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.utils; + +import net.mamoe.mirai.Bot; + +import java.util.List; + +/** + * 获取 Bot Manager + * Java 友好 API + */ +public class BotManager { + + public static List getManagers(long botAccount) { + Bot bot = Bot.getInstance(botAccount); + return getManagers(bot); + } + + public static List getManagers(Bot bot) { + return BotManagers.getManagers(bot); + } + + public static boolean isManager(Bot bot, long target) { + return getManagers(bot).contains(target); + } + + public static boolean isManager(long botAccount, long target) { + return getManagers(botAccount).contains(target); + } +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt new file mode 100644 index 000000000..ae4d730b6 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -0,0 +1,173 @@ +/* + * 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("WRONG_MODIFIER_CONTAINING_DECLARATION", "unused") +@file:OptIn(ConsoleInternalAPI::class) + +package net.mamoe.mirai.console + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.io.charsets.Charset +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsole.INSTANCE +import net.mamoe.mirai.console.command.BuiltInCommands +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.command.internal.InternalCommandManager +import net.mamoe.mirai.console.command.primaryName +import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.console.plugin.center.CuiPluginCenter +import net.mamoe.mirai.console.plugin.center.PluginCenter +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.plugin.jvm.PluginManagerImpl +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.console.utils.ConsoleBuiltInSettingStorage +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.utils.DefaultLogger +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.info +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.PrintStream +import java.text.SimpleDateFormat +import java.util.* +import kotlin.coroutines.CoroutineContext + + +/** + * mirai-console 实例 + * + * @see INSTANCE + */ +public interface MiraiConsole : CoroutineScope { + /** + * Console 运行路径 + */ + public val rootDir: File + + /** + * Console 前端接口 + */ + public val frontEnd: MiraiConsoleFrontEnd + + /** + * 与前端交互所使用的 Logger + */ + public val mainLogger: MiraiLogger + + /** + * 内建加载器列表, 一般需要包含 [JarPluginLoader]. + * + * @return 不可变 [List] ([java.util.Collections.unmodifiableList]) + */ + public val builtInPluginLoaders: List> + + public val buildDate: Date + + public val version: String + + public val pluginCenter: PluginCenter + + @ConsoleExperimentalAPI + public fun newLogger(identity: String?): MiraiLogger + + public companion object INSTANCE : MiraiConsole by MiraiConsoleImplementationBridge { + /** + * 获取 [MiraiConsole] 的 [Job] + */ // MiraiConsole.INSTANCE.getJob() + public val job: Job + get() = MiraiConsole.coroutineContext[Job] + ?: error("Internal error: Job not found in MiraiConsole.coroutineContext") + } +} + +public class IllegalMiraiConsoleImplementationError @JvmOverloads constructor( + public override val message: String? = null, + public override val cause: Throwable? = null +) : Error() + + +internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants) + @JvmStatic + val buildDate: Date = Date(1595136353901L) // 2020-07-19 13:25:53 + const val version: String = "1.0-dev-4" +} + +/** + * [MiraiConsole] 公开 API 与前端实现的连接桥. + */ +internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleImplementation, + MiraiConsole { + override val pluginCenter: PluginCenter get() = CuiPluginCenter + + private val instance: MiraiConsoleImplementation get() = MiraiConsoleImplementation.instance + override val buildDate: Date get() = MiraiConsoleBuildConstants.buildDate + override val version: String get() = MiraiConsoleBuildConstants.version + override val rootDir: File get() = instance.rootDir + override val frontEnd: MiraiConsoleFrontEnd get() = instance.frontEnd + + @ConsoleExperimentalAPI + override val mainLogger: MiraiLogger + get() = instance.mainLogger + override val coroutineContext: CoroutineContext get() = instance.coroutineContext + override val builtInPluginLoaders: List> get() = instance.builtInPluginLoaders + override val consoleCommandSender: ConsoleCommandSender get() = instance.consoleCommandSender + + override val settingStorageForJarPluginLoader: SettingStorage get() = instance.settingStorageForJarPluginLoader + override val settingStorageForBuiltIns: SettingStorage get() = instance.settingStorageForBuiltIns + + init { + DefaultLogger = { identity -> this.newLogger(identity) } + } + + @ConsoleExperimentalAPI + override fun newLogger(identity: String?): MiraiLogger = frontEnd.loggerFor(identity) + + internal fun doStart() { + val buildDateFormatted = SimpleDateFormat("yyyy-MM-dd").format(buildDate) + mainLogger.info { "Starting mirai-console..." } + mainLogger.info { "Backend: version $version, built on $buildDateFormatted." } + mainLogger.info { "Frontend ${frontEnd.name}: version $version." } + + if (coroutineContext[Job] == null) { + throw IllegalMiraiConsoleImplementationError("The coroutineContext given to MiraiConsole must have a Job in it.") + } + MiraiConsole.job.invokeOnCompletion { + Bot.botInstances.forEach { kotlin.runCatching { it.close() }.exceptionOrNull()?.let(mainLogger::error) } + } + + BuiltInCommands.registerAll() + mainLogger.info { "Preparing built-in commands: ${BuiltInCommands.all.joinToString { it.primaryName }}" } + InternalCommandManager.commandListener // start + + mainLogger.info { "Loading plugins..." } + PluginManagerImpl.loadEnablePlugins() + mainLogger.info { "${PluginManager.plugins.size} plugin(s) loaded." } + mainLogger.info { "mirai-console started successfully." } + + ConsoleBuiltInSettingStorage // init + // Only for initialize + } +} + +/** + * Included in kotlin stdlib 1.4 + */ +internal val Throwable.stacktraceString: String + get() = + ByteArrayOutputStream().apply { + printStackTrace(PrintStream(this)) + }.use { it.toByteArray().encodeToString() } + + +@Suppress("NOTHING_TO_INLINE") +internal inline fun ByteArray.encodeToString(charset: Charset = Charsets.UTF_8): String = + kotlinx.io.core.String(this, charset = charset) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt new file mode 100644 index 000000000..1703c38df --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleFrontEnd.kt @@ -0,0 +1,53 @@ +/* + * 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 + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.utils.LoginSolver +import net.mamoe.mirai.utils.MiraiLogger + +/** + * 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console 层 + * + * 需要保证线程安全 + */ +@ConsoleExperimentalAPI +@ConsoleFrontEndImplementation +public interface MiraiConsoleFrontEnd { + /** + * 名称 + */ + public val name: String + + /** + * 版本 + */ + public val version: String + + public fun loggerFor(identity: String?): MiraiLogger + + /** + * 让 UI 层接受一个新的bot + * */ + public fun pushBot( + bot: Bot + ) + + /** + * 让 UI 层提供一个输入, 相当于 [readLine] + */ + public suspend fun requestInput(hint: String): String + + /** + * 由 UI 层创建一个 [LoginSolver] + */ + public fun createLoginSolver(): LoginSolver +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt new file mode 100644 index 000000000..961774f1a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleImplementation.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("unused") + +package net.mamoe.mirai.console + +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.utils.MiraiLogger +import java.io.File +import java.util.concurrent.locks.ReentrantLock +import kotlin.annotation.AnnotationTarget.* + + +/** + * 标记一个仅用于 [MiraiConsole] 前端实现的 API. 这些 API 只应由前端实现者使用, 而不应该被插件或其他调用者使用. + * + * 前端实现时 + */ +@Retention(AnnotationRetention.SOURCE) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) +@MustBeDocumented +public annotation class ConsoleFrontEndImplementation + +/** + * [MiraiConsole] 前端实现, 需低啊用 + */ +@ConsoleFrontEndImplementation +public interface MiraiConsoleImplementation : CoroutineScope { + /** + * Console 运行路径 + */ + public val rootDir: File + + /** + * Console 前端接口 + */ + public val frontEnd: MiraiConsoleFrontEnd + + /** + * 与前端交互所使用的 Logger + */ + public val mainLogger: MiraiLogger + + /** + * 内建加载器列表, 一般需要包含 [JarPluginLoader]. + * + * @return 不可变的 [List] + */ + public val builtInPluginLoaders: List> + + public val consoleCommandSender: ConsoleCommandSender + + public val settingStorageForJarPluginLoader: SettingStorage + public val settingStorageForBuiltIns: SettingStorage + + public companion object { + internal lateinit var instance: MiraiConsoleImplementation + private val initLock = ReentrantLock() + + /** 由前端调用, 初始化 [MiraiConsole] 实例, 并 */ + @JvmStatic + public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock { + this@Companion.instance = this + MiraiConsoleImplementationBridge.doStart() + } + } +} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt similarity index 74% rename from mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt index 8a27c610c..d4eac3ff8 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/BuiltInCommands.kt @@ -9,22 +9,158 @@ package net.mamoe.mirai.console.command -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.isActive +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.Bot -import net.mamoe.mirai.Bot.Companion.botInstances +import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.plugins.PluginManager -import net.mamoe.mirai.console.utils.addManager -import net.mamoe.mirai.console.utils.checkManager -import net.mamoe.mirai.console.utils.managers -import net.mamoe.mirai.console.utils.removeManager -import net.mamoe.mirai.event.subscribeMessages -import net.mamoe.mirai.getFriendOrNull -import net.mamoe.mirai.message.GroupMessageEvent -import net.mamoe.mirai.utils.SimpleLogger -import java.util.* +import net.mamoe.mirai.console.stacktraceString +import net.mamoe.mirai.event.selectMessagesUnit +import net.mamoe.mirai.utils.DirectoryLogger +import net.mamoe.mirai.utils.weeksToMillis +import java.io.File +import kotlin.concurrent.thread +import kotlin.system.exitProcess +/** + * 添加一个 [Bot] 实例到全局 Bot 列表, 但不登录. + */ +public fun MiraiConsole.addBot(id: Long, password: String): Bot { + return Bot(id, password) { + + /** + * 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs]) + * @see DirectoryLogger + * @see redirectNetworkLogToDirectory + */ + fun redirectNetworkLogToDirectory( + dir: File = File("logs"), + retain: Long = 1.weeksToMillis, + identity: (bot: Bot) -> String = { "Net ${it.id}" } + ) { + require(!dir.isFile) { "dir must not be a file" } + dir.mkdirs() + networkLoggerSupplier = { DirectoryLogger(identity(it), dir, retain) } + } + + fun redirectBotLogToDirectory( + dir: File = File("logs"), + retain: Long = 1.weeksToMillis, + identity: (bot: Bot) -> String = { "Net ${it.id}" } + ) { + require(!dir.isFile) { "dir must not be a file" } + dir.mkdirs() + botLoggerSupplier = { DirectoryLogger(identity(it), dir, retain) } + } + + fileBasedDeviceInfo() + this.loginSolver = this@addBot.frontEnd.createLoginSolver() + redirectNetworkLogToDirectory() + // redirectBotLogToDirectory() + } +} + +@Suppress("EXPOSED_SUPER_INTERFACE") +public interface BuiltInCommand : Command, BuiltInCommandInternal + +// for identification +internal interface BuiltInCommandInternal : Command + +@Suppress("unused") +public object BuiltInCommands { + + public val all: Array by lazy { + this::class.nestedClasses.mapNotNull { it.objectInstance as? Command }.toTypedArray() + } + + internal fun registerAll() { + BuiltInCommands::class.nestedClasses.forEach { + (it.objectInstance as? Command)?.register() + } + } + + public object Help : SimpleCommand( + ConsoleCommandOwner, "help", + description = "Gets help about the console." + ), BuiltInCommand { + init { + Runtime.getRuntime().addShutdownHook(thread(false) { + runBlocking { Stop.execute(ConsoleCommandSender.instance) } + }) + } + + @Handler + public suspend fun CommandSender.handle() { + sendMessage("现在有指令: ${allRegisteredCommands.joinToString { it.primaryName }}") + sendMessage("帮助还没写, 将就一下") + } + } + + public object Stop : SimpleCommand( + ConsoleCommandOwner, "stop", "shutdown", "exit", + description = "Stop the whole world." + ), BuiltInCommand { + init { + Runtime.getRuntime().addShutdownHook(thread(false) { + if (!MiraiConsole.isActive) { + return@thread + } + runBlocking { Stop.execute(ConsoleCommandSender.instance) } + }) + } + + private val closingLock = Mutex() + + @Handler + public suspend fun CommandSender.handle(): Unit = closingLock.withLock { + sendMessage("Stopping mirai-console") + kotlin.runCatching { + MiraiConsole.job.cancelAndJoin() + }.fold( + onSuccess = { sendMessage("mirai-console stopped successfully.") }, + onFailure = { + MiraiConsole.mainLogger.error(it) + sendMessage(it.localizedMessage ?: it.message ?: it.toString()) + } + ) + exitProcess(0) + } + } + + public object Login : SimpleCommand( + ConsoleCommandOwner, "login", + description = "Log in a bot account." + ), BuiltInCommand { + @Handler + public suspend fun CommandSender.handle(id: Long, password: String) { + + kotlin.runCatching { + MiraiConsole.addBot(id, password).alsoLogin() + }.fold( + onSuccess = { sendMessage("${it.nick} ($id) Login succeed") }, + onFailure = { throwable -> + sendMessage( + "Login failed: ${throwable.localizedMessage ?: throwable.message ?: throwable.toString()}" + + if (this is MessageEventContextAware<*>) { + this.fromEvent.selectMessagesUnit { + "stacktrace" reply { + throwable.stacktraceString + } + } + "test" + } else "") + + throw throwable + } + ) + } + } +} + +/* /** * Some defaults commands are recommend to be replaced by plugin provided commands @@ -369,3 +505,5 @@ internal object DefaultCommands { } } } + + */ \ No newline at end of file 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 new file mode 100644 index 000000000..ea78d803a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.internal.isValidSubName +import net.mamoe.mirai.message.data.SingleMessage + +/** + * 指令 + * 通常情况下, 你的指令应继承 @see CompositeCommand/SimpleCommand + * @see register 注册这个指令 + * + * @see RawCommand + * @see CompositeCommand + */ +public interface Command { + /** + * 指令名. 需要至少有一个元素. 所有元素都不能带有空格 + */ + public val names: Array + + public val usage: String + + public val description: String + + /** + * 指令权限 + */ + public val permission: CommandPermission + + /** + * 为 `true` 时表示 [指令前缀][CommandPrefix] 可选 + */ + public val prefixOptional: Boolean + + public val owner: CommandOwner + + /** + * @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + * + * @see Command.execute + */ // TODO: 2020/6/28 Java-friendly bridges + public suspend fun CommandSender.onCommand(args: Array) +} + +/** + * [Command] 的基础实现 + */ +public abstract class AbstractCommand @JvmOverloads constructor( + public final override val owner: CommandOwner, + vararg names: String, + description: String = "", + public final override val permission: CommandPermission = CommandPermission.Default, + public final override val prefixOptional: Boolean = false +) : Command { + public final override val description: String = description.trimIndent() + public final override val names: Array = + names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> + list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") } + }.toTypedArray() + +} + +public suspend inline fun Command.onCommand(sender: CommandSender, args: Array): Unit = + sender.run { onCommand(args) } + +/** + * 主要指令名. 为 [Command.names] 的第一个元素. + */ +public val Command.primaryName: String get() = names[0] \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt new file mode 100644 index 000000000..55a10fc9c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecuteResult.kt @@ -0,0 +1,187 @@ +/* + * 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") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.CommandExecuteResult.CommandExecuteStatus +import net.mamoe.mirai.message.data.Message +import kotlin.contracts.contract + +/** + * 指令的执行返回 + * + * @see CommandExecuteStatus + */ +public sealed class CommandExecuteResult { + /** 指令最终执行状态 */ + public abstract val status: CommandExecuteStatus + + /** 指令执行时发生的错误 (如果有) */ + public abstract val exception: Throwable? + + /** 尝试执行的指令 (如果匹配到) */ + public abstract val command: Command? + + /** 尝试执行的指令名 (如果匹配到) */ + public abstract val commandName: String? + + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public abstract val args: Array? + + // abstract val to allow smart casting + + /** 指令执行成功 */ + public class Success( + /** 尝试执行的指令 */ + public override val command: Command, + /** 尝试执行的指令名 */ + public override val commandName: String, + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public override val args: Array + ) : CommandExecuteResult() { + /** 指令执行时发生的错误, 总是 `null` */ + public override val exception: Nothing? get() = null + + /** 指令最终执行状态, 总是 [CommandExecuteStatus.SUCCESSFUL] */ + public override val status: CommandExecuteStatus get() = CommandExecuteStatus.SUCCESSFUL + } + + /** 指令执行过程出现了错误 */ + public class ExecutionException( + /** 指令执行时发生的错误 */ + public override val exception: Throwable, + /** 尝试执行的指令 */ + public override val command: Command, + /** 尝试执行的指令名 */ + public override val commandName: String, + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public override val args: Array + ) : CommandExecuteResult() { + /** 指令最终执行状态, 总是 [CommandExecuteStatus.EXECUTION_EXCEPTION] */ + public override val status: CommandExecuteStatus get() = CommandExecuteStatus.EXECUTION_EXCEPTION + } + + /** 没有匹配的指令 */ + public class CommandNotFound( + /** 尝试执行的指令名 */ + public override val commandName: String + ) : CommandExecuteResult() { + /** 指令执行时发生的错误, 总是 `null` */ + public override val exception: Nothing? get() = null + + /** 尝试执行的指令, 总是 `null` */ + public override val command: Nothing? get() = null + + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public override val args: Nothing? get() = null + + /** 指令最终执行状态, 总是 [CommandExecuteStatus.COMMAND_NOT_FOUND] */ + public override val status: CommandExecuteStatus get() = CommandExecuteStatus.COMMAND_NOT_FOUND + } + + /** 权限不足 */ + public class PermissionDenied( + /** 尝试执行的指令 */ + public override val command: Command, + /** 尝试执行的指令名 */ + public override val commandName: String + ) : CommandExecuteResult() { + /** 基础分割后的实际参数列表, 元素类型可能为 [Message] 或 [String] */ + public override val args: Nothing? get() = null + + /** 指令执行时发生的错误, 总是 `null` */ + public override val exception: Nothing? get() = null + + /** 指令最终执行状态, 总是 [CommandExecuteStatus.PERMISSION_DENIED] */ + public override val status: CommandExecuteStatus get() = CommandExecuteStatus.PERMISSION_DENIED + } + + /** + * 指令的执行状态 + */ + public enum class CommandExecuteStatus { + /** 指令执行成功 */ + SUCCESSFUL, + + /** 指令执行过程出现了错误 */ + EXECUTION_EXCEPTION, + + /** 没有匹配的指令 */ + COMMAND_NOT_FOUND, + + /** 权限不足 */ + PERMISSION_DENIED + } +} + + +@Suppress("RemoveRedundantQualifierName") +public typealias CommandExecuteStatus = CommandExecuteResult.CommandExecuteStatus + +/** + * 当 [this] 为 [CommandExecuteResult.Success] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isSuccess(): Boolean { + contract { + returns(true) implies (this@isSuccess is CommandExecuteResult.Success) + returns(false) implies (this@isSuccess !is CommandExecuteResult.Success) + } + return this is CommandExecuteResult.Success +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isExecutionException(): Boolean { + contract { + returns(true) implies (this@isExecutionException is CommandExecuteResult.ExecutionException) + returns(false) implies (this@isExecutionException !is CommandExecuteResult.ExecutionException) + } + return this is CommandExecuteResult.ExecutionException +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isPermissionDenied(): Boolean { + contract { + returns(true) implies (this@isPermissionDenied is CommandExecuteResult.PermissionDenied) + returns(false) implies (this@isPermissionDenied !is CommandExecuteResult.PermissionDenied) + } + return this is CommandExecuteResult.PermissionDenied +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isCommandNotFound(): Boolean { + contract { + returns(true) implies (this@isCommandNotFound is CommandExecuteResult.CommandNotFound) + returns(false) implies (this@isCommandNotFound !is CommandExecuteResult.CommandNotFound) + } + return this is CommandExecuteResult.CommandNotFound +} + +/** + * 当 [this] 为 [CommandExecuteResult.ExecutionException] 或 [CommandExecuteResult.CommandNotFound] 时返回 `true` + */ +@JvmSynthetic +public fun CommandExecuteResult.isFailure(): Boolean { + contract { + returns(true) implies (this@isFailure !is CommandExecuteResult.Success) + returns(false) implies (this@isFailure is CommandExecuteResult.Success) + } + return this !is CommandExecuteResult.Success +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt new file mode 100644 index 000000000..817e323a4 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandExecutionException.kt @@ -0,0 +1,34 @@ +/* + * 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("MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +/** + * 在 [executeCommand] 中, [Command.onCommand] 抛出异常时包装的异常. + */ +public class CommandExecutionException( + /** + * 执行过程发生异常的指令 + */ + public val command: Command, + /** + * 匹配到的指令名 + */ + public val name: String, + cause: Throwable +) : RuntimeException( + "Exception while executing command '${command.primaryName}'", + cause +) { + public override fun toString(): String = + "CommandExecutionException(command=$command, name='$name')" +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt new file mode 100644 index 000000000..b725bc742 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -0,0 +1,282 @@ +/* + * 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", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE", + "MemberVisibilityCanBePrivate" +) +@file:JvmName("CommandManagerKt") + +package net.mamoe.mirai.console.command + +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import net.mamoe.mirai.console.command.internal.* +import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.MessageChain + +/** + * 指令的所有者. + * @see PluginCommandOwner + */ +public sealed class CommandOwner + +/** + * 插件指令所有者. 插件只能通过 [PluginCommandOwner] 管理指令. + */ +public abstract class PluginCommandOwner(public val plugin: Plugin) : CommandOwner() { + init { + if (plugin is CoroutineScope) { // JVM Plugin + plugin.coroutineContext[Job]?.invokeOnCompletion { + this.unregisterAllCommands() + } + } + } +} + +/** + * 代表控制台所有者. 所有的 mirai-console 内建的指令都属于 [ConsoleCommandOwner]. + */ +public object ConsoleCommandOwner : CommandOwner() + +/** + * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. + * @see JCommandManager.getRegisteredCommands Java 方法 + */ +public val CommandOwner.registeredCommands: List get() = InternalCommandManager.registeredCommands.filter { it.owner == this } + +/** + * 获取所有已经注册了指令列表. + * @see JCommandManager.getRegisteredCommands Java 方法 + */ +public val allRegisteredCommands: List get() = InternalCommandManager.registeredCommands.toList() // copy + +/** + * 指令前缀, 如 '/' + * @see JCommandManager.getCommandPrefix Java 方法 + */ +@get:JvmName("getCommandPrefix") +public val CommandPrefix: String + get() = InternalCommandManager.COMMAND_PREFIX + +/** + * 取消注册所有属于 [this] 的指令 + * @see JCommandManager.unregisterAllCommands Java 方法 + */ +public fun CommandOwner.unregisterAllCommands() { + for (registeredCommand in registeredCommands) { + registeredCommand.unregister() + } +} + +/** + * 注册一个指令. + * + * @param override 是否覆盖重名指令. + * + * 若原有指令 P, 其 [Command.names] 为 'a', 'b', 'c'. + * 新指令 Q, 其 [Command.names] 为 'b', 将会覆盖原指令 A 注册的 'b'. + * + * 即注册完成后, 'a' 和 'c' 将会解析到指令 P, 而 'b' 会解析到指令 Q. + * + * @return + * 若已有重名指令, 且 [override] 为 `false`, 返回 `false`; + * 若已有重名指令, 但 [override] 为 `true`, 覆盖原有指令并返回 `true`. + * + * @see JCommandManager.register Java 方法 + */ +@JvmOverloads +public fun Command.register(override: Boolean = false): Boolean { + if (this is CompositeCommand) this.subCommands // init + + InternalCommandManager.modifyLock.withLock { + if (!override) { + if (findDuplicate() != null) return false + } + InternalCommandManager.registeredCommands.add(this@register) + if (this.prefixOptional) { + for (name in this.names) { + val lowerCaseName = name.toLowerCase() + InternalCommandManager.optionalPrefixCommandMap[lowerCaseName] = this + InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this + } + } else { + for (name in this.names) { + val lowerCaseName = name.toLowerCase() + InternalCommandManager.optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency + InternalCommandManager.requiredPrefixCommandMap[lowerCaseName] = this + } + } + return true + } +} + +/** + * 查找并返回重名的指令. 返回重名指令. + * + * @see JCommandManager.findDuplicate Java 方法 + */ +public fun Command.findDuplicate(): Command? = + InternalCommandManager.registeredCommands.firstOrNull { it.names intersectsIgnoringCase this.names } + +/** + * 取消注册这个指令. 若指令未注册, 返回 `false`. + * + * @see JCommandManager.unregister Java 方法 + */ +public fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { + if (this.prefixOptional) { + this.names.forEach { + InternalCommandManager.optionalPrefixCommandMap.remove(it) + } + } + this.names.forEach { + InternalCommandManager.requiredPrefixCommandMap.remove(it) + } + InternalCommandManager.registeredCommands.remove(this) +} + +/** + * 当 [this] 已经 [注册][register] 后返回 `true` + */ +public fun Command.isRegistered(): Boolean = this in InternalCommandManager.registeredCommands + +//// executing without detailed result (faster) + +/** + * 解析并执行一个指令. 将会检查指令权限, 在无权限时抛出 + * + * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +public suspend fun CommandSender.executeCommand(vararg messages: Any): Command? { + if (messages.isEmpty()) return null + return matchAndExecuteCommandInternal(messages, messages[0].toString().substringBefore(' ')) +} + +/** + * 解析并执行一个指令 + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +@Throws(CommandExecutionException::class) +public suspend fun CommandSender.executeCommand(message: MessageChain): Command? { + if (message.isEmpty()) return null + return matchAndExecuteCommandInternal(message, message[0].toString().substringBefore(' ')) +} + +/** + * 执行一个指令 + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +@JvmOverloads +@Throws(CommandExecutionException::class) +public suspend fun Command.execute(sender: CommandSender, args: MessageChain, checkPermission: Boolean = true) { + sender.executeCommandInternal( + this, + args.flattenCommandComponents().toTypedArray(), + this.primaryName, + checkPermission + ) +} + +/** + * 执行一个指令 + * + * @return 成功执行的指令, 在无匹配指令时返回 `null` + * @throws CommandExecutionException 当 [Command.onCommand] 抛出异常时包装并附带相关指令信息抛出 + * + * @see JCommandManager.executeCommand Java 方法 + */ +@JvmOverloads +@Throws(CommandExecutionException::class) +public suspend fun Command.execute(sender: CommandSender, vararg args: Any, checkPermission: Boolean = true) { + sender.executeCommandInternal( + this, + args.flattenCommandComponents().toTypedArray(), + this.primaryName, + checkPermission + ) +} + +//// execution with detailed result + +/** + * 解析并执行一个指令, 获取详细的指令参数等信息 + * + * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] + * + * @return 执行结果 + * + * @see JCommandManager.executeCommandDetailed Java 方法 + */ +public suspend fun CommandSender.executeCommandDetailed(vararg messages: Any): CommandExecuteResult { + if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("") + return executeCommandDetailedInternal(messages, messages[0].toString().substringBefore(' ')) +} + +/** + * 解析并执行一个指令, 获取详细的指令参数等信息 + * + * 执行过程中产生的异常将不会直接抛出, 而会包装为 [CommandExecuteResult.ExecutionException] + * + * @return 执行结果 + * + * @see JCommandManager.executeCommandDetailed Java 方法 + */ +public suspend fun CommandSender.executeCommandDetailed(messages: MessageChain): CommandExecuteResult { + if (messages.isEmpty()) return CommandExecuteResult.CommandNotFound("") + return executeCommandDetailedInternal(messages, messages[0].toString()) +} + +@JvmSynthetic +internal suspend inline fun CommandSender.executeCommandDetailedInternal( + messages: Any, + commandName: String +): CommandExecuteResult { + val command = + InternalCommandManager.matchCommand(commandName) ?: return CommandExecuteResult.CommandNotFound(commandName) + val args = messages.flattenCommandComponents().dropToTypedArray(1) + + if (!command.testPermission(this)) { + return CommandExecuteResult.PermissionDenied(command, commandName) + } + kotlin.runCatching { + command.onCommand(this, args) + }.fold( + onSuccess = { + return CommandExecuteResult.Success( + commandName = commandName, + command = command, + args = args + ) + }, + onFailure = { + return CommandExecuteResult.ExecutionException( + commandName = commandName, + command = command, + exception = it, + args = args + ) + } + ) +} 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 new file mode 100644 index 000000000..54db97295 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("unused", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.utils.isManager +import net.mamoe.mirai.contact.isAdministrator +import net.mamoe.mirai.contact.isOperator +import net.mamoe.mirai.contact.isOwner + +/** + * 指令权限 + * + * @see AnonymousCommandPermission + */ +public interface CommandPermission { + /** + * 判断 [this] 是否拥有这个指令的权限 + * + * @see CommandSender.hasPermission + * @see CommandPermission.testPermission + */ + public fun CommandSender.hasPermission(): Boolean + + + /** + * 满足两个权限其中一个即可使用指令 + */ // no extension for Java + @JvmDefault + public infix fun or(another: CommandPermission): CommandPermission = OrCommandPermission(this, another) + + /** + * 同时拥有两个权限才能使用指令 + */ // no extension for Java + @JvmDefault + public infix fun and(another: CommandPermission): CommandPermission = AndCommandPermission(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 Administrator : 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 + } + + public object Default : CommandPermission by (Manager or Console) +} + +/** + * 使用 [lambda][block] 快速构造 [CommandPermission] + */ +@JvmSynthetic +@Suppress("FunctionName") +public inline fun AnonymousCommandPermission(crossinline block: CommandSender.() -> Boolean): CommandPermission { + return object : CommandPermission { + override fun CommandSender.hasPermission(): Boolean = block() + } +} + +public inline fun CommandSender.hasPermission(permission: CommandPermission): Boolean = + permission.run { this@hasPermission.hasPermission() } + + +public inline fun CommandPermission.testPermission(sender: CommandSender): Boolean = this.run { sender.hasPermission() } + +public inline fun Command.testPermission(sender: CommandSender): Boolean = sender.hasPermission(this.permission) + +internal class OrCommandPermission( + private val first: CommandPermission, + private val second: CommandPermission +) : CommandPermission { + override fun CommandSender.hasPermission(): Boolean { + return this.hasPermission(first) || this.hasPermission(second) + } +} + + +internal class AndCommandPermission( + private val first: CommandPermission, + private val second: CommandPermission +) : CommandPermission { + override fun CommandSender.hasPermission(): Boolean { + return this.hasPermission(first) && this.hasPermission(second) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.kt new file mode 100644 index 000000000..0e48f667b --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermissionDeniedException.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.command + +/** + * 在 [executeCommand] 中, [CommandSender] 未拥有 [Command.permission] 所要求的权限时抛出的异常. + * + * 总是作为 [CommandExecutionException.cause]. + */ +public class CommandPermissionDeniedException( + /** + * 执行过程发生异常的指令 + */ + public val command: Command +) : RuntimeException("Permission denied while executing command '${command.primaryName}'") { + public override fun toString(): String = + "CommandPermissionDeniedException(command=$command)" +} \ 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 new file mode 100644 index 000000000..b36e0c99a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt @@ -0,0 +1,165 @@ +/* + * 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", "INAPPLICABLE_JVM_NAME") + +package net.mamoe.mirai.console.command + +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.console.utils.JavaFriendlyAPI +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.message.* +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.PlainText + +/** + * 指令发送者 + * + * @see ConsoleCommandSender + * @see UserCommandSender + */ +@Suppress("FunctionName") +public interface CommandSender { + /** + * 与这个 [CommandSender] 相关的 [Bot]. 当通过控制台执行时为 null. + */ + public val bot: Bot? + + /** + * 立刻发送一条消息 + */ + @JvmSynthetic + public suspend fun sendMessage(message: Message) + + @JvmDefault + @JavaFriendlyAPI + @JvmName("sendMessage") + public fun __sendMessageBlocking(messageChain: Message): Unit = runBlocking { sendMessage(messageChain) } + + @JvmDefault + @JavaFriendlyAPI + @JvmName("sendMessage") + public fun __sendMessageBlocking(message: String): Unit = runBlocking { sendMessage(message) } +} + +/** + * 可以知道其 [Bot] 的 [CommandSender] + */ +public interface BotAwareCommandSender : CommandSender { + public override val bot: Bot +} + +public suspend inline fun CommandSender.sendMessage(message: String): Unit = sendMessage(PlainText(message)) + +/** + * 控制台指令执行者. 代表由控制台执行指令 + */ +// 前端实现 +public abstract class ConsoleCommandSender internal constructor() : CommandSender { + public final override val bot: Nothing? get() = null + + public companion object { + internal val instance get() = MiraiConsoleImplementationBridge.consoleCommandSender + } +} + +public fun Friend.asCommandSender(): FriendCommandSender = FriendCommandSender(this) + +public fun Member.asCommandSender(): MemberCommandSender = MemberCommandSender(this) + +public fun User.asCommandSender(): UserCommandSender { + return when (this) { + is Friend -> this.asCommandSender() + is Member -> this.asCommandSender() + else -> error("stub") + } +} + +/** + * 表示由 [MessageEvent] 触发的指令 + */ +public interface MessageEventContextAware : MessageEventExtensions { + public val fromEvent: E +} + +/** + * 代表一个用户私聊机器人执行指令 + * @see User.asCommandSender + */ +public sealed class UserCommandSender : CommandSender, BotAwareCommandSender { + /** + * @see MessageEvent.sender + */ + public abstract val user: User + + /** + * @see MessageEvent.subject + */ + public abstract val subject: Contact + + public override val bot: Bot get() = user.bot + + public final override suspend fun sendMessage(message: Message) { + subject.sendMessage(message) + } +} + +/** + * 代表一个用户私聊机器人执行指令 + * @see Friend.asCommandSender + */ +public open class FriendCommandSender(final override val user: Friend) : UserCommandSender() { + public override val subject: Contact get() = user +} + +/** + * 代表一个用户私聊机器人执行指令 + * @see Friend.asCommandSender + */ +public class FriendCommandSenderOnMessage(override val fromEvent: FriendMessageEvent) : + FriendCommandSender(fromEvent.sender), + MessageEventContextAware, MessageEventExtensions by fromEvent { + public override val subject: Contact get() = super.subject + public override val bot: Bot get() = super.bot +} + +/** + * 代表一个群成员执行指令. + * @see Member.asCommandSender + */ +public open class MemberCommandSender(final override val user: Member) : UserCommandSender() { + public inline val group: Group get() = user.group + public override val subject: Contact get() = group +} + +/** + * 代表一个群成员在群内执行指令. + * @see Member.asCommandSender + */ +public class MemberCommandSenderOnMessage(override val fromEvent: GroupMessageEvent) : + MemberCommandSender(fromEvent.sender), + MessageEventContextAware, MessageEventExtensions by fromEvent { + public override val subject: Contact get() = super.subject + public override val bot: Bot get() = super.bot +} + +/** + * 代表一个群成员通过临时会话私聊机器人执行指令. + * @see Member.asCommandSender + */ +@ConsoleExperimentalAPI +public class TempCommandSenderOnMessage(override val fromEvent: TempMessageEvent) : + MemberCommandSender(fromEvent.sender), + MessageEventContextAware, MessageEventExtensions by fromEvent { + public override val subject: Contact get() = super.subject + public override val bot: Bot get() = super.bot +} \ No newline at end of file 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 new file mode 100644 index 000000000..eddc016a4 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CompositeCommand.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress( + "EXPOSED_SUPER_CLASS", + "NOTHING_TO_INLINE", + "unused", + "WRONG_MODIFIER_TARGET", "CANNOT_WEAKEN_ACCESS_PRIVILEGE", + "WRONG_MODIFIER_CONTAINING_DECLARATION", "RedundantVisibilityModifier" +) + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.description.* +import net.mamoe.mirai.console.command.internal.AbstractReflectionCommand +import net.mamoe.mirai.console.command.internal.CompositeCommandSubCommandAnnotationResolver +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.reflect.KClass + +/** + * 复合指令. + */ +@ConsoleExperimentalAPI +public abstract class CompositeCommand @JvmOverloads constructor( + owner: CommandOwner, + vararg names: String, + description: String = "no description available", + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false, + overrideContext: CommandParserContext = EmptyCommandParserContext +) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), + CommandParserContextAware { + /** + * [CommandArgParser] 的环境 + */ + public final override val context: CommandParserContext = CommandParserContext.Builtins + overrideContext + + /** + * 标记一个函数为子指令, 当 [value] 为空时使用函数名. + * @param value 子指令名 + */ + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class SubCommand(vararg val value: String) + + /** 指定子指令要求的权限 */ + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class Permission(val value: KClass) + + /** 指令描述 */ + @Retention(RUNTIME) + @Target(FUNCTION) + protected annotation class Description(val value: String) + + /** 参数名, 将参与构成 [usage] */ + @Retention(RUNTIME) + @Target(AnnotationTarget.VALUE_PARAMETER) + protected annotation class Name(val value: String) + + public final override suspend fun CommandSender.onCommand(args: Array) { + matchSubCommand(args)?.parseAndExecute(this, args, true) ?: kotlin.run { + defaultSubCommand.onCommand(this, args) + } + } + + + internal override suspend fun CommandSender.onDefault(rawArgs: Array) { + sendMessage(usage) + } + + internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver + get() = CompositeCommandSubCommandAnnotationResolver +} 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 new file mode 100644 index 000000000..a17036fb5 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/RawCommand.kt @@ -0,0 +1,12 @@ +package net.mamoe.mirai.console.command + +public abstract class RawCommand( + public override val owner: CommandOwner, + public override vararg val names: String, + public override val usage: String = "", + public override val description: String = "", + public override val permission: CommandPermission = CommandPermission.Default, + public override val prefixOptional: Boolean = false +) : Command { + public abstract override suspend fun CommandSender.onCommand(args: Array) +} 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 new file mode 100644 index 000000000..67bb7dbf1 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/SimpleCommand.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress( + "EXPOSED_SUPER_CLASS", + "NOTHING_TO_INLINE", + "unused", + "WRONG_MODIFIER_TARGET", "CANNOT_WEAKEN_ACCESS_PRIVILEGE", + "WRONG_MODIFIER_CONTAINING_DECLARATION", "RedundantVisibilityModifier" +) + +package net.mamoe.mirai.console.command + +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.CommandParserContextAware +import net.mamoe.mirai.console.command.description.EmptyCommandParserContext +import net.mamoe.mirai.console.command.description.plus +import net.mamoe.mirai.console.command.internal.AbstractReflectionCommand +import net.mamoe.mirai.console.command.internal.SimpleCommandSubCommandAnnotationResolver + +public abstract class SimpleCommand @JvmOverloads constructor( + owner: CommandOwner, + vararg names: String, + description: String = "no description available", + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false, + overrideContext: CommandParserContext = EmptyCommandParserContext +) : Command, AbstractReflectionCommand(owner, names, description, permission, prefixOptional), + CommandParserContextAware { + + public override val usage: String + get() = super.usage + + /** + * 标注指令处理器 + */ + protected annotation class Handler + + public final override val context: CommandParserContext = CommandParserContext.Builtins + overrideContext + + internal override fun checkSubCommand(subCommands: Array) { + super.checkSubCommand(subCommands) + check(subCommands.size == 1) { "There can only be exactly one function annotated with Handler at this moment as overloading is not yet supported." } + } + + @Deprecated("prohibited", level = DeprecationLevel.HIDDEN) + internal override suspend fun CommandSender.onDefault(rawArgs: Array) = sendMessage(usage) + + public final override suspend fun CommandSender.onCommand(args: Array) { + subCommands.single().parseAndExecute(this, args, false) + } + + internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver + get() = SimpleCommandSubCommandAnnotationResolver +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt new file mode 100644 index 000000000..a5a274eb5 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParser.kt @@ -0,0 +1,87 @@ +/* + * 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") + +package net.mamoe.mirai.console.command.description + +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.content +import kotlin.contracts.contract + +/** + * this output type of that arg + * input is always String + */ +public interface CommandArgParser { + public fun parse(raw: String, sender: CommandSender): T + + @JvmDefault + public fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender) +} + +public fun CommandArgParser.parse(raw: Any, sender: CommandSender): T { + contract { + returns() implies (raw is String || raw is SingleMessage) + } + + return when (raw) { + is String -> parse(raw, sender) + is SingleMessage -> parse(raw, sender) + else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}") + } +} + +@Suppress("unused") +@JvmSynthetic +public inline fun CommandArgParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { + throw ParserException(message, cause) +} + +@JvmSynthetic +public inline fun CommandArgParser<*>.checkArgument( + condition: Boolean, + crossinline message: () -> String = { "Check failed." } +) { + contract { + returns() implies condition + } + if (!condition) illegalArgument(message()) +} + +/** + * 创建匿名 [CommandArgParser] + */ +@Suppress("FunctionName") +@JvmSynthetic +public inline fun CommandArgParser( + crossinline stringParser: CommandArgParser.(s: String, sender: CommandSender) -> T +): CommandArgParser = object : CommandArgParser { + override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender) +} + +/** + * 创建匿名 [CommandArgParser] + */ +@Suppress("FunctionName") +@JvmSynthetic +public inline fun CommandArgParser( + crossinline stringParser: CommandArgParser.(s: String, sender: CommandSender) -> T, + crossinline messageParser: CommandArgParser.(m: SingleMessage, sender: CommandSender) -> T +): CommandArgParser = object : CommandArgParser { + override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender) + override fun parse(raw: SingleMessage, sender: CommandSender): T = messageParser(raw, sender) +} + + +/** + * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范. + */ +public class ParserException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt new file mode 100644 index 000000000..7154af6a6 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt @@ -0,0 +1,238 @@ +/* + * 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.command.description + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.BotAwareCommandSender +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.MemberCommandSender +import net.mamoe.mirai.console.command.UserCommandSender +import net.mamoe.mirai.console.command.internal.fuzzySearchMember +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member +import net.mamoe.mirai.message.data.At +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.content + + +public object IntArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Int = + raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数") +} + +public object LongArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Long = + raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数") +} + +public object ShortArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Short = + raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数") +} + +public object ByteArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Byte = + raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节") +} + +public object DoubleArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Double = + raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数") +} + +public object FloatArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Float = + raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数") +} + +public object StringArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): String { + return raw + } +} + +public object BooleanArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str -> + str.equals("true", ignoreCase = true) + || str.equals("yes", ignoreCase = true) + || str.equals("enabled", ignoreCase = true) + } +} + +/** + * require a bot that already login in console + * input: Bot UIN + * output: Bot + * errors: String->Int convert, Bot Not Exist + */ +public object ExistBotArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Bot { + val uin = raw.toLongOrNull() ?: illegalArgument("无法识别 QQ ID: $raw") + return Bot.getInstanceOrNull(uin) ?: illegalArgument("无法找到 Bot $uin") + } +} + +public object ExistFriendArgParser : CommandArgParser { + //Bot.friend + //friend + //~ = self + public override fun parse(raw: String, sender: CommandSender): Friend { + if (raw == "~") { + if (sender !is BotAwareCommandSender) { + illegalArgument("无法解析~作为默认") + } + val targetID = when (sender) { + is UserCommandSender -> sender.user.id + else -> illegalArgument("无法解析~作为默认") + } + + return sender.bot.friends.getOrNull(targetID) ?: illegalArgument("无法解析~作为默认") + } + if (sender is BotAwareCommandSender) { + return sender.bot.friends.getOrNull(raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为整数")) + ?: illegalArgument("无法找到" + raw + "这个好友") + } else { + raw.split(".").let { args -> + if (args.size != 2) { + illegalArgument("无法解析 $raw, 格式应为 机器人账号.好友账号") + } + return try { + Bot.getInstance(args[0].toLong()).friends.getOrNull( + args[1].toLongOrNull() ?: illegalArgument("无法解析 $raw 为好友") + ) ?: illegalArgument("无法找到好友 ${args[1]}") + } catch (e: NoSuchElementException) { + illegalArgument("无法找到机器人账号 ${args[0]}") + } + } + } + } + + public override fun parse(raw: SingleMessage, sender: CommandSender): Friend { + if (raw is At) { + assert(sender is MemberCommandSender) + return (sender as BotAwareCommandSender).bot.friends.getOrNull(raw.target) ?: illegalArgument("At的对象非Bot好友") + } else { + illegalArgument("无法解析 $raw 为好友") + } + } +} + +public object ExistGroupArgParser : CommandArgParser { + public override fun parse(raw: String, sender: CommandSender): Group { + //by default + if ((raw == "" || raw == "~") && sender is MemberCommandSender) { + return sender.group + } + //from bot to group + if (sender is BotAwareCommandSender) { + val code = try { + raw.toLong() + } catch (e: NoSuchElementException) { + illegalArgument("无法识别Group Code$raw") + } + return try { + sender.bot.getGroup(code) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Group " + code + " from Bot " + sender.bot.id) + } + } + //from console/other + return with(raw.split(".")) { + if (this.size != 2) { + illegalArgument("请使用BotQQ号.群号 来表示Bot的一个群") + } + try { + Bot.getInstance(this[0].toLong()).getGroup(this[1].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到" + this[0] + "的" + this[1] + "群") + } catch (e: NumberFormatException) { + illegalArgument("无法识别群号或机器人UIN") + } + } + } +} + +public object ExistMemberArgParser : CommandArgParser { + //后台: Bot.Group.Member[QQ/名片] + //私聊: Group.Member[QQ/名片] + //群内: Q号 + //群内: 名片 + public override fun parse(raw: String, sender: CommandSender): Member { + if (sender !is BotAwareCommandSender) { + with(raw.split(".")) { + checkArgument(this.size >= 3) { + "无法识别Member, 请使用Bot.Group.Member[QQ/名片]的格式" + } + + val bot = try { + Bot.getInstance(this[0].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Bot") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Bot") + } + + val group = try { + bot.getGroup(this[1].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Group") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Group") + } + + val memberIndex = this.subList(2, this.size).joinToString(".") + return group.members.getOrNull(memberIndex.toLong()) + ?: group.fuzzySearchMember(memberIndex) + ?: error("无法找到成员$memberIndex") + } + } else { + val bot = sender.bot + if (sender is MemberCommandSender) { + val group = sender.group + return try { + group.members[raw.toLong()] + } catch (ignored: Exception) { + group.fuzzySearchMember(raw) ?: illegalArgument("无法找到成员$raw") + } + } else { + with(raw.split(".")) { + if (this.size < 2) { + illegalArgument("无法识别Member, 请使用Group.Member[QQ/名片]的格式") + } + val group = try { + bot.getGroup(this[0].toLong()) + } catch (e: NoSuchElementException) { + illegalArgument("无法找到Group") + } catch (e: NumberFormatException) { + illegalArgument("无法识别Group") + } + + val memberIndex = this.subList(1, this.size).joinToString(".") + return try { + group.members[memberIndex.toLong()] + } catch (ignored: Exception) { + group.fuzzySearchMember(memberIndex) ?: illegalArgument("无法找到成员$memberIndex") + } + } + } + } + } + + public override fun parse(raw: SingleMessage, sender: CommandSender): Member { + return if (raw is At) { + checkArgument(sender is MemberCommandSender) + (sender.group).members[raw.target] + } else { + illegalArgument("无法识别Member" + raw.content) + } + } +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt new file mode 100644 index 000000000..ce7e5413b --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.command.description + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.description.CommandParserContext.ParserPair +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member +import kotlin.internal.LowPriorityInOverloadResolution +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + + +/** + * [KClass] 到 [CommandArgParser] 的匹配 + * @see CustomCommandParserContext 自定义 + */ +public interface CommandParserContext { + public data class ParserPair( + val klass: KClass, + val parser: CommandArgParser + ) + + public operator fun get(klass: KClass): CommandArgParser? + + public fun toList(): List> + + /** + * 内建的默认 [CommandArgParser] + */ + public object Builtins : CommandParserContext by (CommandParserContext { + Int::class with IntArgParser + Byte::class with ByteArgParser + Short::class with ShortArgParser + Boolean::class with BooleanArgParser + String::class with StringArgParser + Long::class with LongArgParser + Double::class with DoubleArgParser + Float::class with FloatArgParser + + Member::class with ExistMemberArgParser + Group::class with ExistGroupArgParser + Bot::class with ExistBotArgParser + Friend::class with ExistFriendArgParser + }) +} + +/** + * 拥有 [CommandParserContext] 的类 + */ +public interface CommandParserContextAware { + /** + * [CommandArgParser] 的环境 + */ + public val context: CommandParserContext +} + +public object EmptyCommandParserContext : CommandParserContext by CustomCommandParserContext(listOf()) + +/** + * 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser. + */ +public operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandParserContext { + if (replacer == EmptyCommandParserContext) return this + if (this == EmptyCommandParserContext) return replacer + return object : CommandParserContext { + override fun get(klass: KClass): CommandArgParser? = replacer[klass] ?: this@plus[klass] + override fun toList(): List> = replacer.toList() + this@plus.toList() + } +} + +/** + * 合并 [this] 与 [replacer], [replacer] 将会替换 [this] 中重复的 parser. + */ +public operator fun CommandParserContext.plus(replacer: List>): CommandParserContext { + if (replacer.isEmpty()) return this + if (this == EmptyCommandParserContext) return CustomCommandParserContext(replacer) + return object : CommandParserContext { + @Suppress("UNCHECKED_CAST") + override fun get(klass: KClass): CommandArgParser? = + replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgParser? ?: this@plus[klass] + + override fun toList(): List> = replacer.toList() + this@plus.toList() + } +} + +@Suppress("UNCHECKED_CAST") +public open class CustomCommandParserContext(public val list: List>) : CommandParserContext { + + override fun get(klass: KClass): CommandArgParser? = + this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgParser? + + override fun toList(): List> { + return list + } +} + +/** + * 构建一个 [CommandParserContext]. + * + * ``` + * CommandParserContext { + * Int::class with IntArgParser + * Member::class with ExistMemberArgParser + * Group::class with { s: String, sender: CommandSender -> + * Bot.getInstance(s.toLong()).getGroup(s.toLong()) + * } + * Bot::class with { s: String -> + * Bot.getInstance(s.toLong()) + * } + * } + * ``` + */ +@Suppress("FunctionName") +@JvmSynthetic +public inline fun CommandParserContext(block: CommandParserContextBuilder.() -> Unit): CommandParserContext { + return CustomCommandParserContext(CommandParserContextBuilder().apply(block).distinctByReversed { it.klass }) +} + +/** + * @see CommandParserContext + */ +public class CommandParserContextBuilder : MutableList> by mutableListOf() { + @JvmName("add") + public inline infix fun KClass.with(parser: CommandArgParser): ParserPair<*> = + ParserPair(this, parser).also { add(it) } + + /** + * 添加一个指令解析器 + */ + @JvmSynthetic + @LowPriorityInOverloadResolution + public inline infix fun KClass.with( + crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T + ): ParserPair<*> = ParserPair(this, CommandArgParser(parser)).also { add(it) } + + /** + * 添加一个指令解析器 + */ + @JvmSynthetic + public inline infix fun KClass.with( + crossinline parser: CommandArgParser.(s: String) -> T + ): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }).also { add(it) } + + @JvmSynthetic + public inline fun add(parser: CommandArgParser): ParserPair<*> = + ParserPair(T::class, parser).also { add(it) } + + /** + * 添加一个指令解析器 + */ + @ConsoleExperimentalAPI + @JvmSynthetic + public inline infix fun add( + crossinline parser: CommandArgParser<*>.(s: String) -> T + ): ParserPair<*> = T::class with CommandArgParser { s: String, _: CommandSender -> parser(s) } + + /** + * 添加一个指令解析器 + */ + @ConsoleExperimentalAPI + @JvmSynthetic + @LowPriorityInOverloadResolution + public inline infix fun add( + crossinline parser: CommandArgParser<*>.(s: String, sender: CommandSender) -> T + ): ParserPair<*> = T::class with CommandArgParser(parser) +} + + +@PublishedApi +internal inline fun List.distinctByReversed(selector: (T) -> K): List { + val set = HashSet() + val list = ArrayList() + for (i in this.indices.reversed()) { + val element = this[i] + if (set.add(element.let(selector))) { + list.add(element) + } + } + return list +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt new file mode 100644 index 000000000..0a641e763 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.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 + */ + +@file:Suppress("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package net.mamoe.mirai.console.command.description + +import net.mamoe.mirai.console.command.CompositeCommand +import java.lang.reflect.Parameter +import kotlin.reflect.KClass + +internal fun Parameter.toCommandParam(): CommandParam<*> { + val name = getAnnotation(CompositeCommand.Name::class.java) + return CommandParam( + name?.value ?: this.name + ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), + this.type.kotlin + ) +} + +/** + * 指令形式参数. + * @see toCommandParam + */ +internal data class CommandParam( + /** + * 参数名. 不允许重复. + */ + val name: String, + /** + * 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgParser] 解析. + */ + val type: KClass // exact type +) { + constructor(name: String, type: KClass, parser: CommandArgParser) : this(name, type) { + this._overrideParser = parser + } + + @Suppress("PropertyName") + @JvmField + internal var _overrideParser: CommandArgParser? = null + + + /** + * 覆盖的 [CommandArgParser]. + * + * 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser] + */ + val overrideParser: CommandArgParser? get() = _overrideParser +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt new file mode 100644 index 000000000..131730b70 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/CompositeCommandInternal.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package net.mamoe.mirai.console.command.internal + +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.command.description.CommandParam +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.CommandParserContextAware +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.SingleMessage +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.full.* + +internal object CompositeCommandSubCommandAnnotationResolver : + AbstractReflectionCommand.SubCommandAnnotationResolver { + override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation() + override fun getSubCommandNames(function: KFunction<*>): Array = + function.findAnnotation()!!.value +} + +internal object SimpleCommandSubCommandAnnotationResolver : + AbstractReflectionCommand.SubCommandAnnotationResolver { + override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation() + override fun getSubCommandNames(function: KFunction<*>): Array = arrayOf("") +} + +internal abstract class AbstractReflectionCommand @JvmOverloads constructor( + owner: CommandOwner, + names: Array, + description: String = "", + permission: CommandPermission = CommandPermission.Default, + prefixOptional: Boolean = false +) : Command, AbstractCommand( + owner, + names = *names, + description = description, + permission = permission, + prefixOptional = prefixOptional +), CommandParserContextAware { + internal abstract val subCommandAnnotationResolver: SubCommandAnnotationResolver + + @JvmField + @Suppress("PropertyName") + internal var _usage: String = "" + + override val usage: String // initialized by subCommand reflection + get() = _usage + + abstract suspend fun CommandSender.onDefault(rawArgs: Array) + + internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { + DefaultSubCommandDescriptor( + "", + permission, + onCommand = block2 { sender: CommandSender, args: Array -> + sender.onDefault(args) + } + ) + } + + internal open fun checkSubCommand(subCommands: Array) { + + } + + interface SubCommandAnnotationResolver { + fun hasAnnotation(function: KFunction<*>): Boolean + fun getSubCommandNames(function: KFunction<*>): Array + } + + internal val subCommands: Array by lazy { + this::class.declaredFunctions.filter { subCommandAnnotationResolver.hasAnnotation(it) } + .also { subCommandFunctions -> + // overloading not yet supported + val overloadFunction = subCommandFunctions.groupBy { it.name }.entries.firstOrNull { it.value.size > 1 } + if (overloadFunction != null) { + error("Sub command overloading is not yet supported. (at ${this::class.qualifiedNameOrTip}.${overloadFunction.key})") + } + }.map { function -> + createSubCommand(function, context) + }.toTypedArray().also { + _usage = it.firstOrNull()?.usage ?: description + }.also { checkSubCommand(it) } + } + + internal val bakedCommandNameToSubDescriptorArray: Map, SubCommandDescriptor> by lazy { + kotlin.run { + val map = LinkedHashMap, SubCommandDescriptor>(subCommands.size * 2) + for (descriptor in subCommands) { + for (name in descriptor.bakedSubNames) { + map[name] = descriptor + } + } + map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) + } + } + + internal class DefaultSubCommandDescriptor( + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Unit + ) + + internal class SubCommandDescriptor( + val names: Array, + val params: Array>, + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean, + val context: CommandParserContext, + val usage: String + ) { + internal suspend inline fun parseAndExecute( + sender: CommandSender, + argsWithSubCommandNameNotRemoved: Array, + removeSubName: Boolean + ) { + val args = parseArgs(sender, argsWithSubCommandNameNotRemoved, if (removeSubName) names.size else 0) + if (args == null || !onCommand( + sender, + args + ) + ) { + sender.sendMessage(usage) + } + } + + @JvmField + internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() + private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array? { + if (rawArgs.size < offset + this.params.size) + return null + //require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } + + return Array(this.params.size) { index -> + val param = params[index] + val rawArg = rawArgs[offset + index] + when (rawArg) { + is String -> context[param.type]?.parse(rawArg, sender) + is SingleMessage -> context[param.type]?.parse(rawArg, sender) + else -> throw IllegalArgumentException("Illegal argument type: ${rawArg::class.qualifiedName}") + } ?: error("Cannot find a parser for $rawArg") + } + } + } + + /** + * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] + */ + internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { + val maxCount = rawArgs.size - 1 + var cur = 0 + bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> + if (name.size != cur) { + if (cur++ == maxCount) return null + } + if (name.contentEqualsOffset(rawArgs, length = cur)) { + return descriptor + } + } + return null + } +} + +internal fun Array.contentEqualsOffset(other: Array, length: Int): Boolean { + repeat(length) { index -> + if (other[index].toString() != this[index]) { + return false + } + } + return true +} + +internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() +internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this } +internal fun String.bakeSubName(): Array = split(' ').filterNot { it.isBlank() }.toTypedArray() + +internal fun Any.flattenCommandComponents(): ArrayList { + val list = ArrayList() + when (this) { + is PlainText -> this.content.splitToSequence(' ').filterNot { it.isBlank() } + .forEach { list.add(it) } + is CharSequence -> this.splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } + is SingleMessage -> list.add(this) + is Array<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + is Iterable<*> -> this.forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + else -> list.add(this.toString()) + } + return list +} + +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 AbstractReflectionCommand.createSubCommand( + function: KFunction<*>, + context: CommandParserContext +): AbstractReflectionCommand.SubCommandDescriptor { + val notStatic = !function.hasAnnotation() + val overridePermission = function.findAnnotation()//optional + val subDescription = + function.findAnnotation()?.value ?: "" + + fun KClass<*>.isValidReturnType(): Boolean { + return when (this) { + Boolean::class, Void::class, Unit::class, Nothing::class -> true + else -> false + } + } + + check((function.returnType.classifier as? KClass<*>)?.isValidReturnType() == true) { + error("Return type of sub command ${function.name} must be one of the following: kotlin.Boolean, java.lang.Boolean, kotlin.Unit (including implicit), kotlin.Nothing, boolean or void (at ${this::class.qualifiedNameOrTip}.${function.name})") + } + + check(!function.returnType.isMarkedNullable) { + error("Return type of sub command ${function.name} must not be marked nullable in Kotlin, and must be marked with @NotNull or @NonNull explicitly in Java. (at ${this::class.qualifiedNameOrTip}.${function.name})") + } + + val parameters = function.parameters.toMutableList() + + if (notStatic) parameters.removeAt(0) // instance + + var hasSenderParam = false + check(parameters.isNotEmpty()) { + "Parameters of sub command ${function.name} must not be empty. (Must have CommandSender as its receiver or first parameter or absent, followed by naturally typed params) (at ${this::class.qualifiedNameOrTip}.${function.name})" + } + + parameters.forEach { param -> + check(!param.isVararg) { + "Parameter $param must not be vararg. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)" + } + } + + (parameters.first()).let { receiver -> + if ((receiver.type.classifier as? KClass<*>)?.isSubclassOf(CommandSender::class) == true) { + hasSenderParam = true + parameters.removeAt(0) + } + } + + val commandName = + subCommandAnnotationResolver.getSubCommandNames(function) + .let { namesFromAnnotation -> + if (namesFromAnnotation.isNotEmpty()) { + namesFromAnnotation + } else arrayOf(function.name) + }.also { names -> + names.forEach { + check(it.isValidSubName()) { + "Name of sub command ${function.name} is invalid" + } + } + } + + val buildUsage = StringBuilder(this.description).append(": \n") + + //map parameter + val params = parameters.map { param -> + buildUsage.append("/$primaryName ") + + if (param.isOptional) error("optional parameters are not yet supported. (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + + val argName = param.findAnnotation()?.value ?: param.name ?: "unknown" + buildUsage.append("<").append(argName).append("> ").append(" ") + CommandParam( + argName, + (param.type.classifier as? KClass<*>) + ?: throw IllegalArgumentException("unsolved type reference from param " + param.name + ". (at ${this::class.qualifiedNameOrTip}.${function.name}.$param)") + ) + }.toTypedArray() + + buildUsage.append(subDescription).append("\n") + + return AbstractReflectionCommand.SubCommandDescriptor( + commandName, + params, + subDescription, + overridePermission?.value?.getInstance() ?: permission, + onCommand = block { sender: CommandSender, args: Array -> + val result = if (notStatic) { + if (hasSenderParam) { + function.isSuspend + function.callSuspend(this, sender, *args) + } else function.callSuspend(this, *args) + } else { + if (hasSenderParam) { + function.callSuspend(sender, *args) + } else function.callSuspend(*args) + } + + checkNotNull(result) { "sub command return value is null (at ${this::class.qualifiedName}.${function.name})" } + + result as? Boolean ?: true // Unit, void is considered as true. + }, + context = context, + usage = buildUsage.toString() + ) +} + + +private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { + return block +} + +private fun block2(block: suspend (CommandSender, Array) -> Unit): suspend (CommandSender, Array) -> Unit { + return block +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt new file mode 100644 index 000000000..4df9b9f1d --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal/internal.kt @@ -0,0 +1,218 @@ +/* + * 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.command.internal + +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.Member +import net.mamoe.mirai.event.Listener +import net.mamoe.mirai.event.subscribeAlways +import net.mamoe.mirai.message.MessageEvent +import java.util.concurrent.locks.ReentrantLock + + +internal infix fun Array.matchesBeginning(list: List): Boolean { + this.forEachIndexed { index, any -> + if (list[index] != any) return false + } + return true +} + +internal object InternalCommandManager : CoroutineScope by CoroutineScope(MiraiConsole.job) { + const val COMMAND_PREFIX = "/" + + @JvmField + internal val registeredCommands: MutableList = mutableListOf() + + /** + * 全部注册的指令 + * /mute -> MuteCommand + * /jinyan -> MuteCommand + */ + @JvmField + internal val requiredPrefixCommandMap: MutableMap = mutableMapOf() + + /** + * Command name of commands that are prefix optional + * mute -> MuteCommand + */ + @JvmField + internal val optionalPrefixCommandMap: MutableMap = mutableMapOf() + + @JvmField + internal val modifyLock = ReentrantLock() + + + /** + * 从原始的 command 中解析出 Command 对象 + */ + internal fun matchCommand(rawCommand: String): Command? { + if (rawCommand.startsWith(COMMAND_PREFIX)) { + return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX).toLowerCase()] + } + return optionalPrefixCommandMap[rawCommand.toLowerCase()] + } + + internal val commandListener: Listener by lazy { + @Suppress("RemoveExplicitTypeArguments") + subscribeAlways( + concurrency = Listener.ConcurrencyKind.CONCURRENT, + priority = Listener.EventPriority.HIGH + ) { + if (this.sender.asCommandSender().executeCommand(message) != null) { + intercept() + } + } + } +} + +internal infix fun Array.intersectsIgnoringCase(other: Array): Boolean { + val max = this.size.coerceAtMost(other.size) + for (i in 0 until max) { + if (this[i].equals(other[i], ignoreCase = true)) return true + } + return false +} + +internal fun String.fuzzyCompare(target: String): Double { + var step = 0 + if (this == target) { + return 1.0 + } + if (target.length > this.length) { + return 0.0 + } + for (i in this.indices) { + if (target.length == i) { + step-- + } else { + if (this[i] != target[i]) { + break + } + step++ + } + } + + if (step == this.length - 1) { + return 1.0 + } + return step.toDouble() / this.length +} + +/** + * 模糊搜索一个List中index最接近target的东西 + */ +internal inline fun Collection.fuzzySearch( + target: String, + index: (T) -> String +): T? { + if (this.isEmpty()) { + return null + } + var potential: T? = null + var rate = 0.0 + this.forEach { + val thisIndex = index(it) + if (thisIndex == target) { + return it + } + with(thisIndex.fuzzyCompare(target)) { + if (this > rate) { + rate = this + potential = it + } + } + } + return potential +} + +/** + * 模糊搜索一个List中index最接近target的东西 + * 并且确保target是唯一的 + * 如搜索index为XXXXYY list中同时存在XXXXYYY XXXXYYYY 将返回null + */ +internal inline fun Collection.fuzzySearchOnly( + target: String, + index: (T) -> String +): T? { + if (this.isEmpty()) { + return null + } + var potential: T? = null + var rate = 0.0 + var collide = 0 + this.forEach { + with(index(it).fuzzyCompare(target)) { + if (this > rate) { + rate = this + potential = it + } + if (this == 1.0) { + collide++ + } + if (collide > 1) { + return null//collide + } + } + } + return potential +} + + +internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { + return this.members.fuzzySearchOnly(nameCardTarget) { + it.nameCard + } +} + + +//// internal + +@JvmSynthetic +internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } + +@JvmSynthetic +@Throws(CommandExecutionException::class) +internal suspend inline fun CommandSender.matchAndExecuteCommandInternal( + messages: Any, + commandName: String +): Command? { + val command = InternalCommandManager.matchCommand( + commandName + ) ?: return null + + this.executeCommandInternal(command, messages.flattenCommandComponents().dropToTypedArray(1), commandName, true) + return command +} + +@JvmSynthetic +@Throws(CommandExecutionException::class) +internal suspend inline fun CommandSender.executeCommandInternal( + command: Command, + args: Array, + commandName: String, + checkPermission: Boolean +) { + if (checkPermission && !command.testPermission(this)) { + throw CommandExecutionException( + command, + commandName, + CommandPermissionDeniedException(command) + ) + } + + kotlin.runCatching { + command.onCommand(this, args) + }.onFailure { + throw CommandExecutionException(command, commandName, it) + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt new file mode 100644 index 000000000..de73126ef --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/CommandExecutionEvent.kt @@ -0,0 +1,38 @@ +/* + * 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.event + +/* +data class CommandExecutionEvent( // TODO: 2020/6/26 impl CommandExecutionEvent + val sender: CommandSender, + val command: Command, + val rawArgs: Array +) : CancellableEvent, ConsoleEvent, AbstractEvent() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CommandExecutionEvent + + if (sender != other.sender) return false + if (command != other.command) return false + if (!rawArgs.contentEquals(other.rawArgs)) return false + + return true + } + + override fun hashCode(): Int { + var result = sender.hashCode() + result = 31 * result + command.hashCode() + result = 31 * result + rawArgs.contentHashCode() + return result + } +} +*/ \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/ConsoleEvent.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/ConsoleEvent.kt new file mode 100644 index 000000000..d5b39d001 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/event/ConsoleEvent.kt @@ -0,0 +1,17 @@ +/* + * 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.event + +import net.mamoe.mirai.event.Event + +/** + * 表示来自 mirai-console 的事件 + */ +public interface ConsoleEvent : Event \ No newline at end of file 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 new file mode 100644 index 000000000..a56ef852a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt @@ -0,0 +1,75 @@ +/* + * 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") + +package net.mamoe.mirai.console.plugin + +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import java.io.File + +/** + * 表示一个 mirai-console 插件. + * + * @see PluginDescription 插件描述 + * @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件 + * @see PluginFileExtensions 支持文件系统存储的扩展 + * + * @see PluginLoader 插件加载器 + */ +public interface Plugin { + /** + * 所属插件加载器实例, 此加载器必须能加载这个 [Plugin]. + */ + public val loader: PluginLoader<*, *> +} + +/** + * 禁用这个插件 + * + * @see PluginLoader.disable + */ +public fun Plugin.disable(): Unit = safeLoader.disable(this) + +/** + * 启用这个插件 + * + * @see PluginLoader.enable + */ +public fun Plugin.enable(): Unit = safeLoader.enable(this) + +/** + * 经过泛型类型转换的 [PluginLoader] + */ +@get:JvmSynthetic +@Suppress("UNCHECKED_CAST") +public inline val

P.safeLoader: PluginLoader + get() = this.loader as PluginLoader + +/** + * 支持文件系统存储的扩展. + * + * @see JvmPlugin + */ +@ConsoleExperimentalAPI("classname is subject to change") +public interface PluginFileExtensions { + /** + * 数据目录 + */ + public val dataFolder: File + + /** + * 从数据目录获取一个文件, 若不存在则创建文件. + */ + @JvmDefault + public fun file(relativePath: String): File = File(dataFolder, relativePath).apply { createNewFile() } + + // TODO: 2020/7/11 add `fun path(...): Path` ? +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt new file mode 100644 index 000000000..1d78f04f6 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginLoader.kt @@ -0,0 +1,106 @@ +/* + * 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", "INAPPLICABLE_JVM_NAME", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.plugin + +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import java.io.File + +/** + * 插件加载器. + * + * 插件加载器只实现寻找插件列表, 加载插件, 启用插件, 关闭插件这四个功能. + * + * 有关插件的依赖和已加载的插件列表由 [PluginManager] 维护. + * + * @see JarPluginLoader Jar 插件加载器 + */ +public interface PluginLoader

{ + /** + * 扫描并返回可以被加载的插件的 [描述][PluginDescription] 列表. 此函数只会被调用一次 + */ + public fun listPlugins(): List + + /** + * 获取此插件的描述 + * + * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等). + */ + @get:JvmName("getPluginDescription") + @get:Throws(PluginLoadException::class) + public val P.description: D // Java signature: `public D getDescription(P)` + + /** + * 加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的主类实例 + * + * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等). + */ + @Throws(PluginLoadException::class) + public fun load(description: D): P + + public fun enable(plugin: P) + public fun disable(plugin: P) +} + +@JvmSynthetic +public inline fun PluginLoader.getDescription(plugin: P): D = + plugin.description + +public open class PluginLoadException : 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) +} + +/** + * '/plugins' 目录中的插件的加载器. 每个加载器需绑定一个后缀. + * + * @see AbstractFilePluginLoader 默认基础实现 + * @see JarPluginLoader 内建的 Jar (JVM) 插件加载器. + */ +public interface FilePluginLoader

: PluginLoader { + /** + * 所支持的插件文件后缀, 含 '.'. 如 [JarPluginLoader] 为 ".jar" + */ + public val fileSuffix: String +} + +/** + * [FilePluginLoader] 的默认基础实现 + */ +public abstract class AbstractFilePluginLoader

( + public override val fileSuffix: String +) : FilePluginLoader { + private fun pluginsFilesSequence(): Sequence = + PluginManager.pluginsDir.walk().filter { it.isFile && it.name.endsWith(fileSuffix, ignoreCase = true) } + + /** + * 读取扫描到的后缀与 [fileSuffix] 相同的文件中的 [PluginDescription] + */ + protected abstract fun Sequence.mapToDescription(): List + + public final override fun listPlugins(): List = pluginsFilesSequence().mapToDescription() +} + + +// Not yet decided to make public API +internal class DeferredPluginLoader

( + initializer: () -> PluginLoader +) : PluginLoader { + private val instance by lazy(initializer) + + override fun listPlugins(): List = instance.listPlugins() + override val P.description: D get() = instance.run { description } + override fun load(description: D): P = instance.load(description) + override fun enable(plugin: P) = instance.enable(plugin) + override fun disable(plugin: P) = instance.disable(plugin) +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt new file mode 100644 index 000000000..d519a3d52 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/PluginManager.kt @@ -0,0 +1,79 @@ +/* + * 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") + +package net.mamoe.mirai.console.plugin + +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.plugin.jvm.PluginManagerImpl +import java.io.File + +/** + * 插件管理器. + */ +public interface PluginManager { + /** + * `$rootDir/plugins` + */ + public val pluginsDir: File + + /** + * `$rootDir/data` + */ + public val pluginsDataFolder: File + + /** + * 已加载的插件列表 + */ + public val plugins: List + + /** + * 内建的插件加载器列表. 由 [MiraiConsole] 初始化. + * + * @return 不可变的 list. + */ + public val builtInLoaders: List> + + /** + * 由插件创建的 [PluginLoader] + */ + public val pluginLoaders: List> + + public fun registerPluginLoader(loader: PluginLoader<*, *>): Boolean + + public fun unregisterPluginLoader(loader: PluginLoader<*, *>): Boolean + + /** + * 获取插件的 [描述][PluginDescription], 通过 [PluginLoader.getDescription] + */ + public val Plugin.description: PluginDescription + + public companion object INSTANCE : PluginManager by PluginManagerImpl +} + +@JvmSynthetic +public inline fun PluginLoader<*, *>.register(): Boolean = PluginManager.registerPluginLoader(this) + +@JvmSynthetic +public inline fun PluginLoader<*, *>.unregister(): Boolean = PluginManager.unregisterPluginLoader(this) + +public class PluginMissingDependencyException : PluginResolutionException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} + +public open class PluginResolutionException : Exception { + 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/center/CuiPluginCenter.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt new file mode 100644 index 000000000..ba2f9bb1e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/CuiPluginCenter.kt @@ -0,0 +1,112 @@ +/* + * 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:OptIn(ConsoleExperimentalAPI::class) + +package net.mamoe.mirai.console.plugin.center + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.get +import io.ktor.util.KtorExperimentalAPI +import kotlinx.serialization.Serializable +import kotlinx.serialization.UnstableDefault +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.console.utils.retryCatching +import java.io.File + +@OptIn(UnstableDefault::class) +internal val json = runCatching { + Json(JsonConfiguration(isLenient = true, ignoreUnknownKeys = true)) +}.getOrElse { Json(JsonConfiguration.Stable) } + +@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.parse(PluginCenter.PluginInfo.serializer(), result) + } + + override suspend fun refresh() { + + @Serializable + data class Result( + val success: Boolean, + val result: List + ) + + val result = json.parse(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/plugin/center/PluginCenter.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt new file mode 100644 index 000000000..8fd84b383 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/center/PluginCenter.kt @@ -0,0 +1,79 @@ +/* + * 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.plugin.center + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import java.io.File + +@ConsoleExperimentalAPI +public interface PluginCenter { + + @Serializable + @ConsoleExperimentalAPI + public data class PluginInsight( + val name: String, + val version: String, + @SerialName("core") + val coreVersion: String, + @SerialName("console") + val consoleVersion: String, + val author: String, + val description: String, + val tags: List, + val commands: List + ) + + @ConsoleExperimentalAPI + @Serializable + public data class PluginInfo( + val name: String, + val version: String, + @SerialName("core") + val coreVersion: String, + @SerialName("console") + val consoleVersion: String, + val tags: List, + val author: String, + val contact: String, + val description: String, + val usage: String, + val vcs: String, + val commands: List, + val changeLog: List + ) + + /** + * 获取一些中心的插件基本信息, + * 能获取到多少由实际的 [PluginCenter] 决定 + * 返回 插件名->Insight + */ + public suspend fun fetchPlugin(page: Int): Map + + /** + * 尝试获取到某个插件 by 全名, case sensitive + * null 则没有 + */ + public suspend fun findPlugin(name: String): PluginInfo? + + + public suspend fun T.downloadPlugin(name: String, progressListener: T.(Float) -> Unit): File + + public suspend fun downloadPlugin(name: String, progressListener: PluginCenter.(Float) -> Unit): File = + downloadPlugin(name, progressListener) + + /** + * 刷新 + */ + public suspend fun refresh() + + public val name: String +} 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 new file mode 100644 index 000000000..1469b446e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description.kt @@ -0,0 +1,101 @@ +/* + * 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.plugin + +import com.vdurmont.semver4j.Semver +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.internal.map +import net.mamoe.mirai.console.utils.SemverAsStringSerializerIvy +import net.mamoe.yamlkt.Yaml +import net.mamoe.yamlkt.YamlDynamicSerializer +import java.io.File + + +/** + * 插件描述 + */ +public interface PluginDescription { + public val kind: PluginKind + + public val name: String + public val author: String + public val version: Semver + public val info: String + + /** 此插件依赖的其他插件, 将会在这些插件加载之后加载此插件 */ + public val dependencies: List<@Serializable(with = PluginDependency.SmartSerializer::class) PluginDependency> +} + +/** 插件类型 */ +@Serializable(with = PluginKind.AsStringSerializer::class) +public enum class PluginKind { + /** 表示此插件提供一个 [PluginLoader], 应在加载其他 [NORMAL] 类型插件前加载 */ + LOADER, + + /** 表示此插件为一个通常的插件, 按照正常的依赖关系加载. */ + NORMAL; + + public object AsStringSerializer : KSerializer by String.serializer().map( + serializer = { it.name }, + deserializer = { str -> + values().firstOrNull { + it.name.equals(str, ignoreCase = true) + } ?: NORMAL + } + ) +} + +/** 插件的一个依赖的信息 */ +@Serializable +public data class PluginDependency( + /** 依赖插件名 */ + public val name: String, + /** + * 依赖版本号. 为 null 时则为不限制版本. + * + * 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/), + * + * 允许 [Apache Ivy 格式版本号](http://ant.apache.org/ivy/history/latest-milestone/ivyfile/dependency.html) + * + * @see versionKind 版本号类型 + */ + public val version: @Serializable(SemverAsStringSerializerIvy::class) Semver? = null, + /** + * 若为 `false`, 插件在找不到此依赖时也能正常加载. + */ + public val isOptional: Boolean = false +) { + public override fun toString(): String { + return "$name v$version" + } + + + /** + * 可支持解析 [String] 作为 [PluginDependency.version] 或单个 [PluginDependency] + */ + public object SmartSerializer : KSerializer by YamlDynamicSerializer.map( + serializer = { it }, + deserializer = { any -> + when (any) { + is Map<*, *> -> Yaml.nonStrict.parse(serializer(), Yaml.nonStrict.stringify(any)) + else -> PluginDependency(any.toString()) + } + } + ) +} + +/** + * 基于文件的插件 的描述 + */ +public interface FilePluginDescription : PluginDescription { + public val file: File +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JarPluginLoaderImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JarPluginLoaderImpl.kt new file mode 100644 index 000000000..df0327f91 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JarPluginLoaderImpl.kt @@ -0,0 +1,117 @@ +/* + * 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.plugin.internal + +import kotlinx.coroutines.* +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader +import net.mamoe.mirai.console.plugin.PluginLoadException +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.yamlkt.Yaml +import java.io.File +import java.net.URI +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.full.createInstance + +internal object JarPluginLoaderImpl : + AbstractFilePluginLoader(".jar"), + CoroutineScope, + JarPluginLoader { + + private val logger: MiraiLogger = MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!) + + @ConsoleExperimentalAPI + override val settingStorage: SettingStorage + get() = MiraiConsoleImplementationBridge.settingStorageForJarPluginLoader + + override val coroutineContext: CoroutineContext = + MiraiConsole.coroutineContext + + SupervisorJob(MiraiConsole.coroutineContext[Job]) + + CoroutineExceptionHandler { _, throwable -> + logger.error("Unhandled Jar plugin exception: ${throwable.message}", throwable) + } + + private val classLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader) + + init { // delayed + coroutineContext[Job]!!.invokeOnCompletion { + classLoader.clear() + } + } + + @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // doesn't matter + override val JvmPlugin.description: JvmPluginDescription + get() = this.description + + override fun Sequence.mapToDescription(): List { + return this.associateWith { URI("jar:file:${it.absolutePath.replace('\\', '/')}!/plugin.yml").toURL() } + .mapNotNull { (file, url) -> + kotlin.runCatching { + url.readText() + }.fold( + onSuccess = { yaml -> + Yaml.nonStrict.parse(JvmPluginDescription.serializer(), yaml) + }, + onFailure = { + logger.error("Cannot load plugin file ${file.name}", it) + null + } + )?.also { it._file = file } + } + } + + @Suppress("RemoveExplicitTypeArguments") // until Kotlin 1.4 NI + @Throws(PluginLoadException::class) + override fun load(description: JvmPluginDescription): JvmPlugin = + description.runCatching { + ensureActive() + val main = classLoader.loadPluginMainClassByJarFile( + pluginName = name, + mainClass = mainClassName, + jarFile = file + ).kotlin.run { + objectInstance + ?: kotlin.runCatching { createInstance() }.getOrNull() + ?: (java.constructors + java.declaredConstructors) + .firstOrNull { it.parameterCount == 0 } + ?.apply { kotlin.runCatching { isAccessible = true } } + ?.newInstance() + } ?: error("No Kotlin object or public no-arg constructor found") + + check(main is JvmPlugin) { "The main class of Jar plugin must extend JvmPlugin, recommending JavaPlugin or KotlinPlugin" } + + if (main is JvmPluginInternal) { + main._description = description + main.internalOnLoad() + } else main.onLoad() + main + }.getOrElse { + throw PluginLoadException("Exception while loading ${description.name}", it) + } + + override fun enable(plugin: JvmPlugin) { + ensureActive() + if (plugin is JvmPluginInternal) { + plugin.internalOnEnable() + } else plugin.onEnable() + } + + override fun disable(plugin: JvmPlugin) { + if (plugin is JvmPluginInternal) { + plugin.internalOnDisable() + } else plugin.onDisable() + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt new file mode 100644 index 000000000..2d066bece --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/JvmPluginInternal.kt @@ -0,0 +1,124 @@ +/* + * 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.plugin.internal + +import kotlinx.atomicfu.AtomicLong +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.utils.ResourceContainer.Companion.asResourceContainer +import net.mamoe.mirai.utils.MiraiLogger +import java.io.File +import java.io.InputStream +import java.util.concurrent.locks.ReentrantLock +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +internal val T.job: Job where T : CoroutineScope, T : Plugin get() = this.coroutineContext[Job]!! + +/** + * Hides implementations from [JvmPlugin] + */ +@PublishedApi +internal abstract class JvmPluginInternal( + parentCoroutineContext: CoroutineContext +) : JvmPlugin, + CoroutineScope { + + private val resourceContainerDelegate by lazy { this::class.asResourceContainer() } + override fun getResourceAsStream(name: String): InputStream = resourceContainerDelegate.getResourceAsStream(name) + + // region JvmPlugin + /** + * Initialized immediately after construction of [JvmPluginInternal] instance + */ + @Suppress("PropertyName") + internal open lateinit var _description: JvmPluginDescription + + override val description: JvmPluginDescription get() = _description + + final override val logger: MiraiLogger by lazy { + MiraiConsole.newLogger( + this._description.name + ) + } + + private var firstRun = true + + override val dataFolder: File by lazy { + File( + PluginManager.pluginsDataFolder, + description.name + ).apply { mkdir() } + } + + internal fun internalOnDisable() { + firstRun = false + this.onDisable() + } + + internal fun internalOnLoad() { + this.onLoad() + } + + internal fun internalOnEnable() { + if (!firstRun) refreshCoroutineContext() + this.onEnable() + } + + // endregion + + // region CoroutineScope + + // for future use + @Suppress("PropertyName") + @JvmField + internal var _intrinsicCoroutineContext: CoroutineContext = + EmptyCoroutineContext + + @JvmField + internal val coroutineContextInitializer = { + CoroutineExceptionHandler { _, throwable -> logger.error(throwable) } + .plus(parentCoroutineContext) + .plus(SupervisorJob(parentCoroutineContext[Job])) + _intrinsicCoroutineContext + } + + private fun refreshCoroutineContext(): CoroutineContext { + return coroutineContextInitializer().also { _coroutineContext = it } + } + + private val contextUpdateLock: ReentrantLock = + ReentrantLock() + private var _coroutineContext: CoroutineContext? = null + final override val coroutineContext: CoroutineContext + get() = _coroutineContext + ?: contextUpdateLock.withLock { _coroutineContext ?: refreshCoroutineContext() } + + // endregion +} + +internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update: (Long) -> Long): Boolean { + while (true) { + val current = value + if (condition(current)) { + if (compareAndSet(0, update(current))) { + return true + } else continue + } + return false + } +} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginsLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/PluginsLoader.kt similarity index 54% rename from mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginsLoader.kt rename to backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/PluginsLoader.kt index 692e9de59..a9a3d9f85 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginsLoader.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/internal/PluginsLoader.kt @@ -7,10 +7,9 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.console.plugins +package net.mamoe.mirai.console.plugin.internal import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.utils.SimpleLogger import java.io.File import java.net.URLClassLoader @@ -18,10 +17,7 @@ internal class PluginsLoader(private val parentClassLoader: ClassLoader) { private val loggerName = "PluginsLoader" private val pluginLoaders = linkedMapOf() private val classesCache = mutableMapOf>() - private val logger = SimpleLogger(loggerName) { p, message, e -> - MiraiConsole.logger(p, "[${loggerName}]", 0, message) - MiraiConsole.logger(p, "[${loggerName}]", 0, e) - } + private val logger = MiraiConsole.newLogger(loggerName) /** * 清除所有插件加载器 @@ -54,7 +50,12 @@ internal class PluginsLoader(private val parentClassLoader: ClassLoader) { fun loadPluginMainClassByJarFile(pluginName: String, mainClass: String, jarFile: File): Class<*> { try { if (!pluginLoaders.containsKey(pluginName)) { - pluginLoaders[pluginName] = PluginClassLoader(pluginName, jarFile, this, parentClassLoader) + pluginLoaders[pluginName] = + PluginClassLoader( + jarFile, + this, + parentClassLoader + ) } return pluginLoaders[pluginName]!!.loadClass(mainClass) } catch (e: ClassNotFoundException) { @@ -70,23 +71,12 @@ internal class PluginsLoader(private val parentClassLoader: ClassLoader) { /** * 尝试加载插件的依赖,无则返回null */ - fun loadDependentClass(name: String): Class<*>? { - var c: Class<*>? = null - // 尝试从缓存中读取 - if (classesCache.containsKey(name)) { - c = classesCache[name] - } - // 然后再交给插件的classloader来加载依赖 - if (c == null) { - pluginLoaders.values.forEach { - try { - c = it.findClass(name, false) - return@forEach - } catch (e: ClassNotFoundException) {/*nothing*/ - } - } - } - return c + fun findClassByName(name: String): Class<*>? { + return classesCache[name] ?: pluginLoaders.values.asSequence().mapNotNull { + kotlin.runCatching { + it.findClass(name, false) + }.getOrNull() + }.firstOrNull() } fun addClassCache(name: String, clz: Class<*>) { @@ -98,59 +88,72 @@ internal class PluginsLoader(private val parentClassLoader: ClassLoader) { } } -internal class PluginClassLoader( - private val pluginName: String, - files: File, - private val pluginsLoader: PluginsLoader, - parent: ClassLoader -) { - private val classesCache = mutableMapOf?>() - private var classLoader: ClassLoader - init { - classLoader = try { - //兼容Android +/** + * A Adapted URL Class Loader that supports Android and JVM for single URL(File) Class Load + */ + +internal open class AdaptiveURLClassLoader(file: File, parent: ClassLoader) : ClassLoader() { + + private val internalClassLoader: ClassLoader by lazy { + kotlin.runCatching { val loaderClass = Class.forName("dalvik.system.PathClassLoader") loaderClass.getConstructor(String::class.java, ClassLoader::class.java) - .newInstance(files.absolutePath, parent) as ClassLoader - } catch (e: ClassNotFoundException) { - URLClassLoader(arrayOf((files.toURI().toURL())), parent) + .newInstance(file.absolutePath, parent) as ClassLoader + }.getOrElse { + URLClassLoader(arrayOf((file.toURI().toURL())), parent) } } - fun loadClass(className: String): Class<*> = classLoader.loadClass(className)!! - - - fun findClass(name: String, isSearchDependent: Boolean = true): Class<*>? { - var clz: Class<*>? = null - // 缓存中找 - if (classesCache.containsKey(name)) { - - return classesCache[name] - } - // 是否寻找依赖 - if (isSearchDependent) { - clz = pluginsLoader.loadDependentClass(name) - } - // 好像没有findClass,直接load - if (clz == null) { - clz = classLoader.loadClass(name) - } - // 加入缓存 - if (clz != null) { - pluginsLoader.addClassCache(name, clz) - } - // 加入缓存 - synchronized(classesCache) { - classesCache[name] = clz - } - return clz + override fun loadClass(name: String?): Class<*> { + return internalClassLoader.loadClass(name) } + + private val internalClassCache = mutableMapOf>() + + internal val classesCache: Map> + get() = internalClassCache + + internal fun addClassCache(string: String, clazz: Class<*>) { + synchronized(internalClassCache) { + internalClassCache[string] = clazz + } + } + + fun close() { - if (classLoader is URLClassLoader) { - (classLoader as URLClassLoader).close() + if (internalClassLoader is URLClassLoader) { + (internalClassLoader as URLClassLoader).close() + } + internalClassCache.clear() + } + +} + +internal class PluginClassLoader( + file: File, + private val pluginsLoader: PluginsLoader, + parent: ClassLoader +) : AdaptiveURLClassLoader(file, parent) { + + override fun findClass(name: String): Class<*> { + return findClass(name, true) + } + + fun findClass(name: String, global: Boolean = true): Class<*> { + return classesCache[name] ?: kotlin.run { + var clazz: Class<*>? = null + if (global) { + clazz = pluginsLoader.findClassByName(name) + } + if (clazz == null) { + clazz = loadClass(name)//这里应该是find, 如果不行就要改 + } + pluginsLoader.addClassCache(name, clazz) + this.addClassCache(name, clazz) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + clazz!! // compiler bug } - classesCache.clear() } } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt new file mode 100644 index 000000000..f5655dd2a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/AbstractJvmPlugin.kt @@ -0,0 +1,32 @@ +/* + * 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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_CLASS") + +package net.mamoe.mirai.console.plugin.jvm + +import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal +import net.mamoe.mirai.utils.minutesToSeconds +import net.mamoe.mirai.utils.secondsToMillis +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * [JavaPlugin] 和 [KotlinPlugin] 的父类 + * + * @see JavaPlugin + * @see KotlinPlugin + */ +public abstract class AbstractJvmPlugin @JvmOverloads constructor( + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext +) : JvmPlugin, JvmPluginInternal(parentCoroutineContext) { + public final override val name: String get() = this.description.name + + public override val autoSaveIntervalMillis: LongRange = 30.secondsToMillis..10.minutesToSeconds +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.kt new file mode 100644 index 000000000..35b2debf4 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JarPluginLoader.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.plugin.jvm + +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.plugin.FilePluginLoader +import net.mamoe.mirai.console.plugin.internal.JarPluginLoaderImpl +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI + +/** + * 内建的 Jar (JVM) 插件加载器 + */ +public interface JarPluginLoader : CoroutineScope, FilePluginLoader { + /** + * [JvmPlugin.loadSetting] 默认使用的实例 + */ + @ConsoleExperimentalAPI + public val settingStorage: SettingStorage + + public companion object INSTANCE : JarPluginLoader by JarPluginLoaderImpl +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPlugin.kt new file mode 100644 index 000000000..ad102d633 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPlugin.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 + */ + +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_CLASS") + +package net.mamoe.mirai.console.plugin.jvm + +import net.mamoe.mirai.console.utils.JavaPluginScheduler +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Java 插件的父类 + */ +public abstract class JavaPlugin @JvmOverloads constructor( + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext +) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) { + + /** + * Java API Scheduler + */ + public val scheduler: JavaPluginScheduler = JavaPluginScheduler(this.coroutineContext) +} \ No newline at end of file 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 new file mode 100644 index 000000000..93ace6ee0 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt @@ -0,0 +1,72 @@ +/* + * 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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.plugin.jvm + +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.plugin.Plugin +import net.mamoe.mirai.console.plugin.PluginFileExtensions +import net.mamoe.mirai.console.setting.AutoSaveSettingHolder +import net.mamoe.mirai.console.setting.Setting +import net.mamoe.mirai.console.utils.ResourceContainer +import net.mamoe.mirai.utils.MiraiLogger +import kotlin.reflect.KClass + + +/** + * Java, Kotlin 或其他 JVM 平台插件 + * + * @see AbstractJvmPlugin 默认实现 + * + * @see JavaPlugin Java 插件 + * @see KotlinPlugin Kotlin 插件 + * + * @see JvmPlugin 支持文件系统扩展 + * @see ResourceContainer 支持资源获取 (如 Jar 中的资源文件) + */ +public interface JvmPlugin : Plugin, CoroutineScope, + PluginFileExtensions, ResourceContainer, AutoSaveSettingHolder { + /** 日志 */ + public val logger: MiraiLogger + + /** 插件描述 */ + public val description: JvmPluginDescription + + /** 所属插件加载器实例 */ + @JvmDefault + public override val loader: JarPluginLoader + get() = JarPluginLoader + + /** + * 获取一个 [Setting] 实例 + */ + @JvmDefault + public fun loadSetting(clazz: Class): T = loader.settingStorage.load(this, clazz) + + // TODO: 2020/7/11 document onLoad, onEnable, onDisable + @JvmDefault + public fun onLoad() { + } + + @JvmDefault + public fun onEnable() { + } + + @JvmDefault + public fun onDisable() { + } +} + +@JvmSynthetic +public inline fun JvmPlugin.loadSetting(clazz: KClass): T = this.loadSetting(clazz.java) + +@JvmSynthetic +public inline fun JvmPlugin.loadSetting(): T = this.loadSetting(T::class) \ No newline at end of file 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 new file mode 100644 index 000000000..e084b5fa8 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.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.plugin.jvm + +import com.vdurmont.semver4j.Semver +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.mamoe.mirai.console.plugin.FilePluginDescription +import net.mamoe.mirai.console.plugin.PluginDependency +import net.mamoe.mirai.console.plugin.PluginDescription +import net.mamoe.mirai.console.plugin.PluginKind +import net.mamoe.mirai.console.utils.SemverAsStringSerializerLoose +import java.io.File + +@Serializable +public class JvmPluginDescription internal constructor( + public override val kind: PluginKind = PluginKind.NORMAL, + public override val name: String, + @SerialName("main") + public val mainClassName: String, + public override val author: String = "", + public override val version: @Serializable(with = SemverAsStringSerializerLoose::class) Semver, + public override val info: String = "", + @SerialName("depends") + public override val dependencies: List<@Serializable(with = PluginDependency.SmartSerializer::class) PluginDependency> = listOf() +) : PluginDescription, FilePluginDescription { + + /** + * 在手动实现时使用这个构造器. + */ + @Suppress("unused") + public constructor( + kind: PluginKind, name: String, mainClassName: String, author: String, + version: Semver, info: String, depends: List, + file: File + ) : this(kind, name, mainClassName, author, version, info, depends) { + this._file = file + } + + public override val file: File + get() = _file ?: error("Internal error: JvmPluginDescription(name=$name)._file == null") + + + @Suppress("PropertyName") + @Transient + @JvmField + internal var _file: File? = null + + public override fun toString(): String { + return "JvmPluginDescription(kind=$kind, name='$name', mainClassName='$mainClassName', author='$author', version='$version', info='$info', dependencies=$dependencies, _file=$_file)" + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/KotlinPlugin.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/KotlinPlugin.kt new file mode 100644 index 000000000..a8e14e9e4 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/KotlinPlugin.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 + */ + +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_CLASS", "RedundantVisibilityModifier") + +package net.mamoe.mirai.console.plugin.jvm + +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Kotlin 插件的父类. + * + * 必须通过 "plugin.yml" 指定主类并由 [JarPluginLoader] 加载. + */ +public abstract class KotlinPlugin @JvmOverloads constructor( + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext +) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) + + +/** + * 在内存动态加载的插件. + */ +@ConsoleExperimentalAPI +public abstract class KotlinMemoryPlugin @JvmOverloads constructor( + description: JvmPluginDescription, + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext +) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) { + internal final override var _description: JvmPluginDescription + get() = super._description + set(value) { + super._description = value + } + + init { + _description = description + } +} + +/* + +public object MyPlugin : KotlinPlugin() + +public object AccountSetting : Setting by MyPlugin.getSetting() { + public val s by value(1) +} +*/ \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/PluginManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/PluginManagerImpl.kt new file mode 100644 index 000000000..937868985 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/PluginManagerImpl.kt @@ -0,0 +1,193 @@ +/* + * 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") + +package net.mamoe.mirai.console.plugin.jvm + +import kotlinx.atomicfu.locks.withLock +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.plugin.* +import net.mamoe.mirai.console.setting.internal.cast +import net.mamoe.mirai.utils.info +import java.io.File +import java.util.concurrent.locks.ReentrantLock + +internal object PluginManagerImpl : PluginManager { + override val pluginsDir = File(MiraiConsole.rootDir, "plugins").apply { mkdir() } + override val pluginsDataFolder = File(MiraiConsole.rootDir, "data").apply { mkdir() } + + @Suppress("ObjectPropertyName") + private val _pluginLoaders: MutableList> = mutableListOf() + private val loadersLock: ReentrantLock = ReentrantLock() + private val logger = MiraiConsole.newLogger("PluginManager") + + @JvmField + internal val resolvedPlugins: MutableList = mutableListOf() + override val plugins: List + get() = resolvedPlugins.toList() + override val builtInLoaders: List> + get() = MiraiConsole.builtInPluginLoaders + override val pluginLoaders: List> + get() = _pluginLoaders.toList() + + override val Plugin.description: PluginDescription + get() = resolvedPlugins.firstOrNull { it == this } + ?.loader?.cast>() + ?.getDescription(this) + ?: error("Plugin is unloaded") + + override fun registerPluginLoader(loader: PluginLoader<*, *>): Boolean = loadersLock.withLock { + if (_pluginLoaders.any { it::class == loader }) { + return false + } + _pluginLoaders.add(loader) + } + + override fun unregisterPluginLoader(loader: PluginLoader<*, *>) = loadersLock.withLock { + _pluginLoaders.remove(loader) + } + + + // region LOADING + + private fun

PluginLoader.loadPluginNoEnable(description: D): P { + return kotlin.runCatching { + this.load(description).also { resolvedPlugins.add(it) } + }.fold( + onSuccess = { + logger.info { "Successfully loaded plugin ${description.name}" } + it + }, + onFailure = { + logger.info { "Cannot load plugin ${description.name}" } + throw it + } + ) + } + + private fun

PluginLoader.enablePlugin(plugin: Plugin) { + kotlin.runCatching { + @Suppress("UNCHECKED_CAST") + this.enable(plugin as P) + }.fold( + onSuccess = { + logger.info { "Successfully enabled plugin ${plugin.description.name}" } + }, + onFailure = { + logger.info { "Cannot enable plugin ${plugin.description.name}" } + throw it + } + ) + } + + /** + * 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() + _pluginLoaders.listAllPlugins().flatMap { it.second }) + .sortByDependencies().loadAndEnableAllInOrder() + } + + private fun List.loadAndEnableAllInOrder() { + return this.map { (loader, desc) -> + loader to loader.loadPluginNoEnable(desc) + }.forEach { (loader, plugin) -> + loader.enablePlugin(plugin) + } + } + + /** + * @return [builtInLoaders] 可以加载的插件. 已经完成了 [PluginLoader.load], 但没有 [PluginLoader.enable] + */ + @Suppress("UNCHECKED_CAST") + @Throws(PluginMissingDependencyException::class) + private fun loadAndEnableLoaderProviders(): List { + val allDescriptions = + this.builtInLoaders.listAllPlugins() + .asSequence() + .onEach { (loader, descriptions) -> + loader as PluginLoader + + descriptions.filter { it.kind == PluginKind.LOADER }.sortByDependencies().loadAndEnableAllInOrder() + } + .flatMap { it.second.asSequence() } + + return allDescriptions.toList() + } + + private fun List>.listAllPlugins(): List, List>> { + return associateWith { loader -> loader.listPlugins().map { desc -> desc.wrapWith(loader) } }.toList() + } + + @Throws(PluginMissingDependencyException::class) + private fun List.sortByDependencies(): List { + val resolved = ArrayList(this.size) + + fun D.canBeLoad(): Boolean = this.dependencies.all { it.isOptional || it in resolved } + + fun List.consumeLoadable(): List { + val (canBeLoad, cannotBeLoad) = this.partition { it.canBeLoad() } + resolved.addAll(canBeLoad) + return cannotBeLoad + } + + fun List.filterIsMissing(): List = + this.filterNot { it.isOptional || it in resolved } + + tailrec fun List.doSort() { + if (this.isEmpty()) return + + val beforeSize = this.size + this.consumeLoadable().also { resultPlugins -> + check(resultPlugins.size < beforeSize) { + throw PluginMissingDependencyException(resultPlugins.joinToString("\n") { badPlugin -> + "Cannot load plugin ${badPlugin.name}, missing dependencies: ${ + badPlugin.dependencies.filterIsMissing() + .joinToString() + }" + }) + } + }.doSort() + } + + this.doSort() + return resolved + } + + // endregion +} + +internal data class PluginDescriptionWithLoader( + @JvmField val loader: PluginLoader<*, PluginDescription>, // easier type + @JvmField val delegate: PluginDescription +) : PluginDescription by delegate + +@Suppress("UNCHECKED_CAST") +internal fun PluginDescription.unwrap(): D = + if (this is PluginDescriptionWithLoader) this.delegate as D else this as D + +@Suppress("UNCHECKED_CAST") +internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>): PluginDescriptionWithLoader = + PluginDescriptionWithLoader( + loader as PluginLoader<*, PluginDescription>, this + ) + +internal operator fun List.contains(dependency: PluginDependency): Boolean = + any { it.name == dependency.name } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt new file mode 100644 index 000000000..a48f5225d --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Setting.kt @@ -0,0 +1,177 @@ +/* + * 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("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS") + +package net.mamoe.mirai.console.setting + +import kotlinx.serialization.KSerializer +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.plugin.jvm.loadSetting +import net.mamoe.mirai.console.setting.internal.* +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import kotlin.internal.LowPriorityInOverloadResolution +import kotlin.reflect.KProperty +import kotlin.reflect.KType + + +/** + * 序列化之后的名称. + * + * 例: + * ``` + * @SerialName("accounts") + * object AccountSettings : Setting by ... { + * @SerialName("info") + * val map: Map by value("a" to "b") + * } + * ``` + * + * 将被保存为配置 (YAML 作为示例): + * ```yaml + * accounts: + * info: + * a: b + * ``` + */ +public typealias SerialName = kotlinx.serialization.SerialName + +/** + * [Setting] 的默认实现. 支持使用 `by value()` 等委托方法创建 [Value] 并跟踪其改动. + * + * @see Setting + */ +public abstract class AbstractSetting : Setting, SettingImpl() { + /** + * 使用 `by` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪. + */ + public final override operator fun SerializerAwareValue.provideDelegate( + thisRef: Any?, + property: KProperty<*> + ): SerializerAwareValue { + val name = property.serialName + valueNodes.add(Node(name, this, this.serializer)) + return this + } + + /** + * 值更新序列化器. 仅供内部使用 + */ + @ConsoleInternalAPI + public final override val updaterSerializer: KSerializer + get() = super.updaterSerializer + + /** + * 当所属于这个 [Setting] 的 [Value] 的 [值][Value.value] 被修改时被调用. + */ + public abstract override fun onValueChanged(value: Value<*>) +} + +/** + * 一个配置对象. 可包含对多个 [Value] 的值变更的跟踪. + * + * 在 [JvmPlugin] 的实现方式: + * ``` + * object PluginMain : KotlinPlugin() + * + * object AccountSettings : Setting by PluginMain.getSetting() { + * val map: Map by value("a" to "b") + * } + * ``` + * + * @see JvmPlugin.loadSetting 通过 [JvmPlugin] 获取指定 [Setting] 实例. + */ +public interface Setting : ExperimentalSettingExtensions { + /** + * 使用 `by` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪. + */ + public operator fun SerializerAwareValue.provideDelegate( + thisRef: Any?, + property: KProperty<*> + ): SerializerAwareValue + + /** + * 值更新序列化器. 仅供内部使用 + */ + public val updaterSerializer: KSerializer + + /** + * 当所属于这个 [Setting] 的 [Value] 的 [值][Value.value] 被修改时被调用. + */ + public fun onValueChanged(value: Value<*>) + + /** + * 当这个 [Setting] 被放入一个 [SettingStorage] 时调用 + */ + public fun setStorage(storage: SettingStorage) +} + +@ConsoleExperimentalAPI("") +public interface ExperimentalSettingExtensions { + public fun MutableMap.shadowMap( + eToK: (E) -> K, + kToE: (K) -> E + ): MutableMap { + return this.shadowMap( + kTransform = eToK, + kTransformBack = kToE, + vTransform = { it }, + vTransformBack = { it } + ) + } +} + +//// region Setting_value_primitives CODEGEN //// + +public fun Setting.value(default: Byte): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Short): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Int): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Long): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Float): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Double): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Char): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: Boolean): SerializerAwareValue = valueImpl(default) +public fun Setting.value(default: String): SerializerAwareValue = valueImpl(default) + +//// endregion Setting_value_primitives CODEGEN //// + + +/** + * Creates a [Value] with reified type, and set default value. + * + * @param T reified param type T. + * Supports only primitives, Kotlin built-in collections, + * and classes that are serializable with Kotlinx.serialization + * (typically annotated with [kotlinx.serialization.Serializable]) + */ +@Suppress("UNCHECKED_CAST") +@LowPriorityInOverloadResolution +public inline fun Setting.value(default: T): SerializerAwareValue = valueFromKType(typeOf0(), default) + +/** + * Creates a [Value] with reified type, and set default value by reflection to its no-arg public constructor. + * + * @param T reified param type T. + * Supports only primitives, Kotlin built-in collections, + * and classes that are serializable with Kotlinx.serialization + * (typically annotated with [kotlinx.serialization.Serializable]) + */ +@LowPriorityInOverloadResolution +public inline fun Setting.value(): SerializerAwareValue = value(T::class.createInstance() as T) + +/** + * Creates a [Value] with specified [KType], and set default value. + */ +@Suppress("UNCHECKED_CAST") +@ConsoleExperimentalAPI +public fun Setting.valueFromKType(type: KType, default: T): SerializerAwareValue = + (valueFromKTypeImpl(type) as SerializerAwareValue).apply { this.value = default } as SerializerAwareValue + +// TODO: 2020/6/24 Introduce class TypeToken for compound types for Java. \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt new file mode 100644 index 000000000..f9ea37d63 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/SettingStorage.kt @@ -0,0 +1,187 @@ +/* + * 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", "UNCHECKED_CAST", "unused") + +package net.mamoe.mirai.console.setting + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.setting.SettingStorage.Companion.load +import net.mamoe.mirai.console.setting.internal.* +import java.io.File +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.createType + +/** + * [Setting] 存储容器. + * + * 此为较低层的 API, 一般插件开发者不会接触. + * + * [JarPluginLoader] 实现一个 [SettingStorage], 用于管理所有 [JvmPlugin] 的 [Setting] 实例. + * + * @see SettingHolder + * @see JarPluginLoader.settingStorage + */ +public interface SettingStorage { + /** + * 读取一个实例. 在 [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage] + */ + public fun load(holder: SettingHolder, settingClass: Class): T + + /** + * 保存一个实例 + */ + public fun store(holder: SettingHolder, setting: Setting) + + public companion object { + /** + * 读取一个实例. 在 [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage] + */ + @JvmStatic + public fun SettingStorage.load(holder: SettingHolder, settingClass: KClass): T = + this.load(holder, settingClass.java) + + /** + * 读取一个实例. 在 [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage] + */ + @JvmSynthetic + public inline fun SettingStorage.load(holder: SettingHolder): T = + this.load(holder, T::class) + } +} + +/** + * 在内存存储所有 [Setting] 实例的 [SettingStorage]. 在内存数据丢失后相关 [Setting] 实例也会丢失. + */ +public interface MemorySettingStorage : SettingStorage, Map, Setting> { + /** + * 当任一 [Setting] 实例拥有的 [Value] 的值被改变后调用的回调函数. + */ + public fun interface OnChangedCallback { + public fun onChanged(storage: MemorySettingStorage, value: Value<*>) + + /** + * 无任何操作的 [OnChangedCallback] + * @see OnChangedCallback + */ + public object NoOp : OnChangedCallback { + public override fun onChanged(storage: MemorySettingStorage, value: Value<*>) { + // no-op + } + } + } + + public companion object { + /** + * 创建一个 [MemorySettingStorage] 实例. + * + * @param onChanged 当任一 [Setting] 实例拥有的 [Value] 的值被改变后调用的回调函数. + */ + @JvmStatic + @JvmName("create") + @JvmOverloads + public operator fun invoke(onChanged: OnChangedCallback = OnChangedCallback.NoOp): MemorySettingStorage = + MemorySettingStorageImpl(onChanged) + } +} + +/** + * 在内存存储所有 [Setting] 实例的 [SettingStorage]. + */ +public interface MultiFileSettingStorage : SettingStorage { + /** + * 存放 [Setting] 的目录. + */ + public val directory: File + + public companion object { + /** + * 创建一个 [MultiFileSettingStorage] 实例. + * + * @see directory 存放 [Setting] 的目录. + */ + @JvmStatic + @JvmName("create") + public operator fun invoke(directory: File): MultiFileSettingStorage = MultiFileSettingStorageImpl(directory) + } +} + +/** + * 可以持有相关 [Setting] 实例的对象, 作为 [Setting] 实例的拥有者. + * + * @see SettingStorage.load + * @see SettingStorage.store + * + * @see AutoSaveSettingHolder 自动保存 + */ +public interface SettingHolder { + /** + * 保存时使用的分类名 + */ + public val name: String + + /** + * 创建一个 [Setting] 实例. + * + * @see Companion.newSettingInstance + * @see KClass.createType + */ + @JvmDefault + public fun newSettingInstance(type: KType): T = + newSettingInstanceUsingReflection(type) as T + + public companion object { + /** + * 创建一个 [Setting] 实例. + * + * @see SettingHolder.newSettingInstance + */ + @JvmSynthetic + public inline fun SettingHolder.newSettingInstance(): T { + return this.newSettingInstance(typeOf0()) + } + } +} + +/** + * 可以持有相关 [AutoSaveSetting] 的对象. + * + * @see net.mamoe.mirai.console.plugin.jvm.JvmPlugin + */ +public interface AutoSaveSettingHolder : SettingHolder, CoroutineScope { + /** + * [AutoSaveSetting] 每次自动保存时间间隔 + * + * - 区间的左端点为最小间隔, 一个 [Value] 被修改后, 若此时间段后无其他修改, 将触发自动保存; 若有, 将重新开始计时. + * - 区间的右端点为最大间隔, 一个 [Value] 被修改后, 最多不超过这个时间段后就会被保存. + * + * 若 [AutoSaveSettingHolder.coroutineContext] 含有 [Job], + * 则 [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] 在 Job 完结时触发自动保存. + * + * @see LongRange Java 用户使用 [LongRange] 的构造器创建 + * @see Long.rangeTo Kotlin 用户使用 [Long.rangeTo] 创建, 如 `3000..50000` + */ + public val autoSaveIntervalMillis: LongRange + + /** + * 仅支持确切的 [Setting] 类型 + */ + @JvmDefault + public override fun newSettingInstance(type: KType): T { + val classifier = type.classifier?.cast>()?.java + require(classifier == Setting::class.java) { + "Cannot create Setting instance. AutoSaveSettingHolder supports only Setting type." + } + return AutoSaveSetting(this) as T // T is always Setting + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt new file mode 100644 index 000000000..07ff85f8c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/Value.kt @@ -0,0 +1,256 @@ +/* + * 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("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "unused", "NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.setting + +import kotlinx.serialization.BinaryFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat +import net.mamoe.mirai.console.setting.internal.map +import net.mamoe.mirai.console.setting.internal.setValueBySerializer +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import kotlin.reflect.KProperty + +/** + * Represents a observable, immutable value wrapping. + * + * The value can be modified by delegation just like Kotlin's `var`, however it can also be done by the user, e.g. changing using the UI frontend. + * + * Some frequently used types are specially treated with performance enhancement by codegen. + * + * @see PrimitiveValue + * @see CompositeValue + */ +public interface Value { + public var value: T +} + +/** + * Typically returned by [Setting.value] functions. + */ +public class SerializableValue( + private val delegate: Value, + /** + * The serializer used to update and dump [delegate] + */ + public override val serializer: KSerializer +) : Value by delegate, SerializerAwareValue { + public override fun toString(): String = delegate.toString() + + public companion object { + @JvmStatic + @JvmName("create") + public fun Value.serializableValueWith( + serializer: KSerializer + ): SerializableValue { + return SerializableValue( + this, + serializer.map(serializer = { this.value }, deserializer = { this.setValueBySerializer(it) }) + ) + } + } +} + +/** + * @see SerializableValue + */ +public interface SerializerAwareValue : Value { + public val serializer: KSerializer + + public companion object { + @JvmStatic + @ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization") + public fun SerializerAwareValue.serialize(format: StringFormat): String { + return format.stringify(this.serializer, Unit) + } + + @JvmStatic + @ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization") + public fun SerializerAwareValue.serialize(format: BinaryFormat): ByteArray { + return format.dump(this.serializer, Unit) + } + + @JvmStatic + @ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization") + public fun SerializerAwareValue.deserialize(format: StringFormat, value: String) { + format.parse(this.serializer, value) + } + + @JvmStatic + @ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization") + public fun SerializerAwareValue.deserialize(format: BinaryFormat, value: ByteArray) { + format.load(this.serializer, value) + } + } +} + +@JvmSynthetic +public inline operator fun Value.getValue(mySetting: Any?, property: KProperty<*>): T = value + +@JvmSynthetic +public inline operator fun Value.setValue(mySetting: Any?, property: KProperty<*>, value: T) { + this.value = value +} + +/** + * The serializer for a specific kind of [Value]. + */ +public typealias ValueSerializer = KSerializer> + +/** + * Represents a observable *primitive* value wrapping. + * + * 9 types that are considered *primitive*: + * - Integers: [Byte], [Short], [Int], [Long] + * - Floating: [Float], [Double] + * - [Boolean] + * - [Char], [String] + * + * Note: The values are actually *boxed* because of the generic type T. + * *Primitive* indicates only it is one of the 9 types mentioned above. + */ +public interface PrimitiveValue : Value + + +//// region PrimitiveValues CODEGEN //// + +/** + * Represents a non-null [Byte] value. + */ +public interface ByteValue : PrimitiveValue + +/** + * Represents a non-null [Short] value. + */ +public interface ShortValue : PrimitiveValue + +/** + * Represents a non-null [Int] value. + */ +public interface IntValue : PrimitiveValue + +/** + * Represents a non-null [Long] value. + */ +public interface LongValue : PrimitiveValue + +/** + * Represents a non-null [Float] value. + */ +public interface FloatValue : PrimitiveValue + +/** + * Represents a non-null [Double] value. + */ +public interface DoubleValue : PrimitiveValue + +/** + * Represents a non-null [Char] value. + */ +public interface CharValue : PrimitiveValue + +/** + * Represents a non-null [Boolean] value. + */ +public interface BooleanValue : PrimitiveValue + +/** + * Represents a non-null [String] value. + */ +public interface StringValue : PrimitiveValue + +//// endregion PrimitiveValues CODEGEN //// + + +@ConsoleExperimentalAPI +public interface CompositeValue : Value + + +/** + * Superclass of [CompositeListValue], [PrimitiveListValue]. + */ +public interface ListValue : CompositeValue> + +/** + * Elements can by anything, wrapped as [Value]. + * @param E is not primitive types. + */ +public interface CompositeListValue : ListValue + +/** + * Elements can only be primitives, not wrapped. + * @param E is not primitive types. + */ +public interface PrimitiveListValue : ListValue + + +//// region PrimitiveListValue CODEGEN //// + +public interface PrimitiveIntListValue : PrimitiveListValue +public interface PrimitiveLongListValue : PrimitiveListValue +// TODO + codegen + +//// endregion PrimitiveListValue CODEGEN //// + + +/** + * Superclass of [CompositeSetValue], [PrimitiveSetValue]. + */ +public interface SetValue : CompositeValue> + +/** + * Elements can by anything, wrapped as [Value]. + * @param E is not primitive types. + */ +public interface CompositeSetValue : SetValue + +/** + * Elements can only be primitives, not wrapped. + * @param E is not primitive types. + */ +public interface PrimitiveSetValue : SetValue + + +//// region PrimitiveSetValue CODEGEN //// + +public interface PrimitiveIntSetValue : PrimitiveSetValue +public interface PrimitiveLongSetValue : PrimitiveSetValue +// TODO + codegen + +//// endregion PrimitiveSetValue CODEGEN //// + + +/** + * Superclass of [CompositeMapValue], [PrimitiveMapValue]. + */ +public interface MapValue : CompositeValue> + +public interface CompositeMapValue : MapValue + +public interface PrimitiveMapValue : MapValue + + +//// region PrimitiveMapValue CODEGEN //// + +public interface PrimitiveIntIntMapValue : PrimitiveMapValue +public interface PrimitiveIntLongMapValue : PrimitiveMapValue +// TODO + codegen + +//// endregion PrimitiveSetValue CODEGEN //// + + + + + + + + + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/CompositeValueImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/CompositeValueImpl.kt new file mode 100644 index 000000000..5b908f218 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/CompositeValueImpl.kt @@ -0,0 +1,199 @@ +/* + * 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") + +package net.mamoe.mirai.console.setting.internal + +import net.mamoe.mirai.console.setting.* + + +// type inference bug +internal fun Setting.createCompositeSetValueImpl(tToValue: (T) -> Value): CompositeSetValueImpl { + return object : CompositeSetValueImpl(tToValue) { + override fun onChanged() { + this@createCompositeSetValueImpl.onValueChanged(this) + } + } +} + +internal abstract class CompositeSetValueImpl( + tToValue: (T) -> Value // should override onChanged +) : CompositeSetValue, AbstractValueImpl>() { + private val internalSet: MutableSet> = mutableSetOf() + + private var _value: Set = internalSet.shadowMap({ it.value }, tToValue).observable { onChanged() } + + override var value: Set + get() = _value + set(v) { + if (_value != v) { + @Suppress("LocalVariableName") + val _value = _value as MutableSet + _value.clear() + _value.addAll(v) + onChanged() + } + } + + override fun setValueBySerializer(value: Set) { + val thisValue = this.value + if (!thisValue.tryPatch(value)) { + this.value = value // deep set + } + } + + protected abstract fun onChanged() + override fun toString(): String = _value.toString() + override fun equals(other: Any?): Boolean = + other is CompositeSetValueImpl<*> && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return value.hashCode() * 31 + super.hashCode() + } +} + + +// type inference bug +internal fun Setting.createCompositeListValueImpl(tToValue: (T) -> Value): CompositeListValueImpl { + return object : CompositeListValueImpl(tToValue) { + override fun onChanged() { + this@createCompositeListValueImpl.onValueChanged(this) + } + } +} + +internal abstract class CompositeListValueImpl( + tToValue: (T) -> Value // should override onChanged +) : CompositeListValue, AbstractValueImpl>() { + private val internalList: MutableList> = mutableListOf() + + private val _value: List = internalList.shadowMap({ it.value }, tToValue).observable { onChanged() } + + override var value: List + get() = _value + set(v) { + if (_value != v) { + @Suppress("LocalVariableName") + val _value = _value as MutableList + _value.clear() + _value.addAll(v) + onChanged() + } + } + + override fun setValueBySerializer(value: List) { + val thisValue = this.value + if (!thisValue.tryPatch(value)) { + this.value = value // deep set + } + } + + protected abstract fun onChanged() + override fun toString(): String = _value.toString() + override fun equals(other: Any?): Boolean = + other is CompositeListValueImpl<*> && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return value.hashCode() * 31 + super.hashCode() + } +} + +// workaround to a type inference bug +internal fun Setting.createCompositeMapValueImpl( + kToValue: (K) -> Value, + vToValue: (V) -> Value +): CompositeMapValueImpl { + return object : CompositeMapValueImpl(kToValue, vToValue) { + override fun onChanged() = this@createCompositeMapValueImpl.onValueChanged(this) + } +} + +// TODO: 2020/6/24 在一个 Value 被删除后停止追踪其更新. + +internal abstract class CompositeMapValueImpl( + kToValue: (K) -> Value, // should override onChanged + vToValue: (V) -> Value // should override onChanged +) : CompositeMapValue, AbstractValueImpl>() { + private val internalList: MutableMap, Value> = mutableMapOf() + + private var _value: MutableMap = + internalList.shadowMap({ it.value }, kToValue, { it.value }, vToValue).observable { onChanged() } + override var value: Map + get() = _value + set(v) { + if (_value != v) { + @Suppress("LocalVariableName") + val _value = _value + _value.clear() + _value.putAll(v) + onChanged() + } + } + + override fun setValueBySerializer(value: Map) { + val thisValue = this.value as MutableMap + if (!thisValue.tryPatch(value)) { + this.value = value // deep set + } + } + + protected abstract fun onChanged() + override fun toString(): String = _value.toString() + override fun equals(other: Any?): Boolean = + other is CompositeMapValueImpl<*, *> && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return value.hashCode() * 31 + super.hashCode() + } +} + +internal fun MutableMap.patchImpl(_new: Map) { + val new = _new.toMutableMap() + val iterator = this.iterator() + for (entry in iterator) { + val newValue = new.remove(entry.key) + + if (newValue != null) { + // has replacer + if (entry.value?.tryPatch(newValue) != true) { + // patch not supported, or old value is null + entry.setValue(newValue) + } // else: patched, no remove + } else { + // no replacer + iterator.remove() + } + } + putAll(new) +} + +internal fun , E> C.patchImpl(_new: Collection) { + this.retainAll(_new) +} + +/** + * True if successfully patched + */ +@Suppress("UNCHECKED_CAST") +internal fun Any.tryPatch(any: Any): Boolean = when { + this is MutableCollection<*> && any is Collection<*> -> { + (this as MutableCollection).patchImpl(any as Collection) + true + } + this is MutableMap<*, *> && any is Map<*, *> -> { + (this as MutableMap).patchImpl(any as Map) + true + } + this is Value<*> && any is Value<*> -> any.value?.let { otherValue -> this.value?.tryPatch(otherValue) } == true + else -> false +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/Setting.value composite impl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/Setting.value composite impl.kt new file mode 100644 index 000000000..a7fc1299a --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/Setting.value composite impl.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package net.mamoe.mirai.console.setting.internal + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.* +import net.mamoe.mirai.console.setting.SerializableValue.Companion.serializableValueWith +import net.mamoe.mirai.console.setting.SerializerAwareValue +import net.mamoe.mirai.console.setting.Setting +import net.mamoe.mirai.console.setting.valueFromKType +import net.mamoe.yamlkt.YamlDynamicSerializer +import net.mamoe.yamlkt.YamlNullableDynamicSerializer +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.createInstance as createInstanceKotlin + +private val primitiveCollectionsImplemented by lazy { + false +} + +@PublishedApi +@OptIn(ExperimentalStdlibApi::class) +internal inline fun typeOf0(): KType = kotlin.reflect.typeOf() + +@PublishedApi +@Suppress("UnsafeCall", "SMARTCAST_IMPOSSIBLE", "UNCHECKED_CAST") +internal fun Setting.valueFromKTypeImpl(type: KType): SerializerAwareValue<*> { + val classifier = type.classifier + require(classifier is KClass<*>) + + if (classifier.isPrimitiveOrBuiltInSerializableValue()) { + return valueImplPrimitive(classifier) as SerializerAwareValue<*> + } + + // TODO: 2020/6/24 优化性能: 预先根据类型生成 V -> Value 的 mapper + + when (classifier) { + MutableMap::class, + Map::class, + LinkedHashMap::class, + HashMap::class + -> { + val keyClass = type.arguments[0].type?.classifier + require(keyClass is KClass<*>) + + val valueClass = type.arguments[1].type?.classifier + require(valueClass is KClass<*>) + + if (primitiveCollectionsImplemented && keyClass.isPrimitiveOrBuiltInSerializableValue() && valueClass.isPrimitiveOrBuiltInSerializableValue()) { + // PrimitiveIntIntMap + // ... + TODO() + } else { + return createCompositeMapValueImpl( + kToValue = { k -> valueFromKType(type.arguments[0].type!!, k) }, + vToValue = { v -> valueFromKType(type.arguments[1].type!!, v) } + ).serializableValueWith(serializerMirai(type) as KSerializer>) // erased + } + } + MutableList::class, + List::class, + ArrayList::class + -> { + val elementClass = type.arguments[0].type?.classifier + require(elementClass is KClass<*>) + + if (primitiveCollectionsImplemented && elementClass.isPrimitiveOrBuiltInSerializableValue()) { + // PrimitiveIntList + // ... + TODO() + } else { + return createCompositeListValueImpl { v -> valueFromKType(type.arguments[0].type!!, v) } + .serializableValueWith(serializerMirai(type) as KSerializer>) + } + } + MutableSet::class, + Set::class, + LinkedHashSet::class, + HashSet::class + -> { + val elementClass = type.arguments[0].type?.classifier + require(elementClass is KClass<*>) + + if (primitiveCollectionsImplemented && elementClass.isPrimitiveOrBuiltInSerializableValue()) { + // PrimitiveIntSet + // ... + TODO() + } else { + return createCompositeSetValueImpl { v -> valueFromKType(type.arguments[0].type!!, v) } + .serializableValueWith(serializerMirai(type) as KSerializer>) + } + } + else -> error("Custom composite value is not supported yet (${classifier.qualifiedName})") + } +} + +@PublishedApi +internal fun KClass<*>.createInstance(): Any? { + return when (this) { + MutableMap::class, + Map::class, + LinkedHashMap::class, + HashMap::class + -> mutableMapOf() + + MutableList::class, + List::class, + ArrayList::class + -> mutableListOf() + + MutableSet::class, + Set::class, + LinkedHashSet::class, + HashSet::class + -> mutableSetOf() + + else -> createInstanceKotlin() + } +} + +internal fun KClass<*>.isPrimitiveOrBuiltInSerializableValue(): Boolean { + when (this) { + Byte::class, Short::class, Int::class, Long::class, + Boolean::class, + Char::class, String::class, + Pair::class, Triple::class // TODO: 2020/6/24 支持 PairValue, TripleValue + -> return true + } + + return false +} + +@PublishedApi +@Suppress("UNCHECKED_CAST") +internal inline fun Any.cast(): R = this as R + +/** + * Copied from kotlinx.serialization, modifications are marked with "/* mamoe modify */" + * Copyright 2017-2020 JetBrains s.r.o. + */ +@Suppress( + "UNCHECKED_CAST", + "NO_REFLECTION_IN_CLASS_PATH", + "UNSUPPORTED", + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "IMPLICIT_CAST_TO_ANY" +) +@OptIn(ImplicitReflectionSerializer::class) +internal fun serializerMirai(type: KType): KSerializer { + fun serializerByKTypeImpl(type: KType): KSerializer { + val rootClass = when (val t = type.classifier) { + is KClass<*> -> t + else -> error("Only KClass supported as classifier, got $t") + } as KClass + + val typeArguments = type.arguments + .map { requireNotNull(it.type) { "Star projections are not allowed, had $it instead" } } + return when { + typeArguments.isEmpty() -> rootClass.serializer() + else -> { + val serializers = typeArguments + .map(::serializer) + // Array is not supported, see KT-32839 + when (rootClass) { + List::class, MutableList::class, ArrayList::class -> ListSerializer(serializers[0]) + HashSet::class -> SetSerializer(serializers[0]) + Set::class, MutableSet::class, LinkedHashSet::class -> SetSerializer(serializers[0]) + HashMap::class -> MapSerializer(serializers[0], serializers[1]) + Map::class, MutableMap::class, LinkedHashMap::class -> MapSerializer(serializers[0], serializers[1]) + Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1]) + Pair::class -> PairSerializer(serializers[0], serializers[1]) + Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2]) + /* mamoe modify */ Any::class -> if (type.isMarkedNullable) YamlNullableDynamicSerializer else YamlDynamicSerializer + else -> { + if (isReferenceArray(type, rootClass)) { + @Suppress("RemoveExplicitTypeArguments") + return ArraySerializer( + typeArguments[0].classifier as KClass, + serializers[0] + ).cast() + } + requireNotNull(rootClass.constructSerializerForGivenTypeArgs(*serializers.toTypedArray())) { + "Can't find a method to construct serializer for type ${rootClass.simpleName()}. " + + "Make sure this class is marked as @Serializable or provide serializer explicitly." + } + } + } + } + }.cast() + } + + val result = serializerByKTypeImpl(type) + return if (type.isMarkedNullable) result.nullable else result.cast() +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt new file mode 100644 index 000000000..44e4a10b4 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingImpl.kt @@ -0,0 +1,129 @@ +/* + * 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("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS") + +package net.mamoe.mirai.console.setting.internal + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.SerializerAwareValue +import net.mamoe.mirai.console.setting.Setting +import net.mamoe.mirai.console.setting.Value +import net.mamoe.yamlkt.YamlNullableDynamicSerializer +import kotlin.reflect.KProperty +import kotlin.reflect.full.findAnnotation + +internal val KProperty<*>.serialName: String get() = this.findAnnotation()?.value ?: this.name + +/** + * Internal implementation for [Setting] including: + * - Reflection on Kotlin properties and Java fields + * - Auto-saving + */ +internal abstract class SettingImpl { + internal fun findNodeInstance(name: String): Node<*>? = valueNodes.firstOrNull { it.serialName == name } + + internal data class Node( + val serialName: String, + val value: Value, + val updaterSerializer: KSerializer + ) + + internal fun SerializerAwareValue.provideDelegateImpl( + property: KProperty<*> + ): SerializerAwareValue { + val name = property.serialName + valueNodes.add(Node(name, this, this.serializer)) + return this + } + + internal val valueNodes: MutableList> = mutableListOf() + + internal open val updaterSerializer: KSerializer = object : KSerializer { + override val descriptor: SerialDescriptor get() = settingUpdaterSerializerDescriptor + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder) { + val descriptor = descriptor + with(decoder.beginStructure(descriptor, *settingUpdaterSerializerTypeArguments)) { + if (decodeSequentially()) { + var index = 0 + repeat(decodeCollectionSize(descriptor)) { + val serialName = decodeSerializableElement(descriptor, index++, String.serializer()) + val node = findNodeInstance(serialName) + if (node == null) { + decodeSerializableElement(descriptor, index++, YamlNullableDynamicSerializer) + } else { + decodeSerializableElement(descriptor, index++, node.updaterSerializer) + } + } + } else { + outerLoop@ while (true) { + var serialName: String? = null + innerLoop@ while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.READ_DONE) { + check(serialName == 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()) + } else { + check(serialName != null) { "name must not be null at this moment" } + + val node = findNodeInstance(serialName) + if (node == null) { + decodeSerializableElement(descriptor, index, YamlNullableDynamicSerializer) + } else { + decodeSerializableElement(descriptor, index, node.updaterSerializer) + } + + + break@innerLoop + } + } + + } + } + endStructure(descriptor) + } + } + + @Suppress("UNCHECKED_CAST") + override fun serialize(encoder: Encoder, value: Unit) { + val descriptor = descriptor + with(encoder.beginCollection(descriptor, valueNodes.size, *settingUpdaterSerializerTypeArguments)) { + var index = 0 + + // val vSerializer = settingUpdaterSerializerTypeArguments[1] as KSerializer + valueNodes.forEach { (serialName, _, valueSerializer) -> + encodeSerializableElement(descriptor, index++, String.serializer(), serialName) + encodeSerializableElement(descriptor, index++, valueSerializer, Unit) + } + endStructure(descriptor) + } + } + + } + + /** + * flatten + */ + abstract fun onValueChanged(value: Value<*>) + + companion object { + private val settingUpdaterSerializerTypeArguments = arrayOf(String.serializer(), YamlNullableDynamicSerializer) + private val settingUpdaterSerializerDescriptor = + MapSerializer(settingUpdaterSerializerTypeArguments[0], settingUpdaterSerializerTypeArguments[1]).descriptor + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingStorage internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingStorage internal.kt new file mode 100644 index 000000000..b4c2ec01d --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/SettingStorage internal.kt @@ -0,0 +1,190 @@ +/* + * 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.setting.internal + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.mamoe.mirai.console.command.internal.qualifiedNameOrTip +import net.mamoe.mirai.console.plugin.internal.updateWhen +import net.mamoe.mirai.console.plugin.jvm.loadSetting +import net.mamoe.mirai.console.setting.* +import net.mamoe.mirai.console.utils.ConsoleExperimentalAPI +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.utils.currentTimeMillis +import net.mamoe.yamlkt.Yaml +import java.io.File +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.full.findAnnotation + + +/** + * 链接自动保存的 [Setting]. + * 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存 + * + * 若 [AutoSaveSettingHolder.coroutineContext] 含有 [Job], 则 [AutoSaveSetting] 会通过 [Job.invokeOnCompletion] 在 Job 完结时触发自动保存. + * + * @see loadSetting + */ +internal open class AutoSaveSetting(private val owner: AutoSaveSettingHolder) : + AbstractSetting() { + private lateinit var storage: SettingStorage + + override fun setStorage(storage: SettingStorage) { + check(!this::storage.isInitialized) { "storage is already initialized" } + this.storage = storage + } + + @JvmField + @Volatile + internal var lastAutoSaveJob: Job? = null + + @JvmField + @Volatile + internal var currentFirstStartTime = atomic(0L) + + init { + owner.coroutineContext[Job]?.invokeOnCompletion { doSave() } + } + + private val updaterBlock: suspend CoroutineScope.() -> Unit = { + currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis }) + + 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() + } + } + + @Suppress("RedundantVisibilityModifier") + @ConsoleInternalAPI + public final override fun onValueChanged(value: Value<*>) { + lastAutoSaveJob = owner.launch(block = updaterBlock) + } + + private fun doSave() = storage.store(owner, this) +} + +internal class MemorySettingStorageImpl( + private val onChanged: MemorySettingStorage.OnChangedCallback +) : SettingStorage, MemorySettingStorage, + MutableMap, Setting> by mutableMapOf() { + + internal inner class MemorySettingImpl : AbstractSetting() { + @ConsoleInternalAPI + override fun onValueChanged(value: Value<*>) { + onChanged.onChanged(this@MemorySettingStorageImpl, value) + } + + override fun setStorage(storage: SettingStorage) { + check(storage is MemorySettingStorageImpl) { "storage is not MemorySettingStorageImpl" } + } + } + + @Suppress("UNCHECKED_CAST") + override fun load(holder: SettingHolder, settingClass: Class): T = (synchronized(this) { + this.getOrPut(settingClass) { + settingClass.kotlin.run { + objectInstance ?: createInstanceOrNull() ?: kotlin.run { + if (settingClass != Setting::class.java) { + throw IllegalArgumentException( + "Cannot create Setting instance. Make sure settingClass is Setting::class.java or a Kotlin's object, " + + "or has a constructor which either has no parameters or all parameters of which are optional" + ) + } + MemorySettingImpl() + } + } + } + } as T).also { it.setStorage(this) } + + override fun store(holder: SettingHolder, setting: Setting) { + synchronized(this) { + this[setting::class.java] = setting + } + } +} + +@Suppress("RedundantVisibilityModifier") // might be public in the future +internal open class MultiFileSettingStorageImpl( + public final override val directory: File +) : SettingStorage, MultiFileSettingStorage { + public override fun load(holder: SettingHolder, settingClass: Class): T = + with(settingClass.kotlin) { + val file = getSettingFile(holder, settingClass::class) + + @Suppress("UNCHECKED_CAST") + val instance = objectInstance ?: this.createInstanceOrNull() ?: kotlin.run { + if (settingClass != Setting::class.java) { + throw IllegalArgumentException( + "Cannot create Setting instance. Make sure settingClass is Setting::class.java or a Kotlin's object, " + + "or has a constructor which either has no parameters or all parameters of which are optional" + ) + } + if (holder is AutoSaveSettingHolder) { + AutoSaveSetting(holder) as T? + } else null + } ?: throw IllegalArgumentException( + "Cannot create Setting instance. Make sure 'holder' is a AutoSaveSettingHolder, " + + "or 'setting' is an object or has a constructor which either has no parameters or all parameters of which are optional" + ) + if (file.exists() && file.isFile && file.canRead()) { + Yaml.default.parse(instance.updaterSerializer, file.readText()) + } + instance + } + + protected open fun getSettingFile(holder: SettingHolder, clazz: KClass<*>): File = with(clazz) { + val name = findASerialName() + + val dir = File(directory, holder.name) + if (dir.isFile) { + error("Target directory ${dir.path} for holder $holder is occupied by a file therefore setting $qualifiedNameOrTip can't be saved.") + } + + val file = File(directory, name) + if (file.isDirectory) { + error("Target file $file is occupied by a directory therefore setting $qualifiedNameOrTip can't be saved.") + } + return file + } + + @ConsoleExperimentalAPI + public override fun store(holder: SettingHolder, setting: Setting): Unit = with(setting::class) { + val file = getSettingFile(holder, this) + + if (file.exists() && file.isFile && file.canRead()) { + file.writeText(Yaml.default.stringify(setting.updaterSerializer, Unit)) + } + } +} + +@JvmSynthetic +internal fun KClass.createInstanceOrNull(): T? { + val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) } + ?: return null + + return noArgsConstructor.callBy(emptyMap()) +} + +@JvmSynthetic +internal fun KClass<*>.findASerialName(): String = + findAnnotation()?.value + ?: qualifiedName + ?: throw IllegalArgumentException("Cannot find a serial name for $this") \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_PrimitiveValueDeclarations.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_PrimitiveValueDeclarations.kt new file mode 100644 index 000000000..dd5b0af2e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_PrimitiveValueDeclarations.kt @@ -0,0 +1,345 @@ +/* + * 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.setting.internal + +import kotlinx.serialization.Decoder +import kotlinx.serialization.Encoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialDescriptor +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.* + +/** + * The super class to all ValueImpl s + */ +internal abstract class AbstractValueImpl : Value { + open fun setValueBySerializer(value: T) { + this.value = value + } +} + +internal fun Value.setValueBySerializer(value: T) = (this as AbstractValueImpl).setValueBySerializer(value) + +//// region PrimitiveValuesImpl CODEGEN //// + +internal abstract class ByteValueImpl : ByteValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Byte) { + _value = default + } + + private var _value: Byte? = null + + final override var value: Byte + get() = _value ?: error("ByteValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.ByteSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Byte.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Byte.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "ByteValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is ByteValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class ShortValueImpl : ShortValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Short) { + _value = default + } + + private var _value: Short? = null + + final override var value: Short + get() = _value ?: error("ShortValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.ShortSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Short.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Short.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "ShortValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is ShortValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} +internal abstract class IntValueImpl : IntValue, SerializerAwareValue, KSerializer, AbstractValueImpl { + constructor() + constructor(default: Int) { + _value = default + } + + private var _value: Int? = null + + final override var value: Int + get() = _value ?: error("IntValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.IntSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Int.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Int.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "IntValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is IntValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class LongValueImpl : LongValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Long) { + _value = default + } + + private var _value: Long? = null + + final override var value: Long + get() = _value ?: error("LongValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.LongSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Long.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Long.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "LongValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is LongValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class FloatValueImpl : FloatValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Float) { + _value = default + } + + private var _value: Float? = null + + final override var value: Float + get() = _value ?: error("FloatValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.FloatSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Float.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Float.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "FloatValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is FloatValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class DoubleValueImpl : DoubleValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Double) { + _value = default + } + + private var _value: Double? = null + + final override var value: Double + get() = _value ?: error("DoubleValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.DoubleSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Double.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Double.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "DoubleValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is DoubleValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class CharValueImpl : CharValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Char) { + _value = default + } + + private var _value: Char? = null + + final override var value: Char + get() = _value ?: error("CharValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.CharSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Char.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Char.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "CharValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is CharValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class BooleanValueImpl : BooleanValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: Boolean) { + _value = default + } + + private var _value: Boolean? = null + + final override var value: Boolean + get() = _value ?: error("BooleanValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.BooleanSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = Boolean.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(Boolean.serializer().deserialize(decoder)) + override fun toString(): String = _value?.toString() ?: "BooleanValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is BooleanValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +internal abstract class StringValueImpl : StringValue, SerializerAwareValue, KSerializer, + AbstractValueImpl { + constructor() + constructor(default: String) { + _value = default + } + + private var _value: String? = null + + final override var value: String + get() = _value ?: error("StringValue.value should be initialized before get.") + set(v) { + if (v != this._value) { + this._value = v + onChanged() + } + } + + protected abstract fun onChanged() + + final override val serializer: KSerializer get() = this + final override val descriptor: SerialDescriptor get() = BuiltInSerializerConstants.StringSerializerDescriptor + final override fun serialize(encoder: Encoder, value: Unit) = String.serializer().serialize(encoder, this.value) + final override fun deserialize(decoder: Decoder) = setValueBySerializer(String.serializer().deserialize(decoder)) + override fun toString(): String = _value ?: "StringValue.value not yet initialized." + override fun equals(other: Any?): Boolean = + other is StringValueImpl && other::class.java == this::class.java && other._value == this._value + + override fun hashCode(): Int { + val value = _value + return if (value == null) 1 + else value.hashCode() * 31 + } +} + +//// endregion PrimitiveValuesImpl CODEGEN //// diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_Setting.value.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_Setting.value.kt new file mode 100644 index 000000000..8415b2f58 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/_Setting.value.kt @@ -0,0 +1,165 @@ +/* + * 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.setting.internal + +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.SerializerAwareValue +import net.mamoe.mirai.console.setting.Setting +import kotlin.reflect.KClass + + +internal object BuiltInSerializerConstants { + //// region BuiltInSerializerConstantsPrimitives CODEGEN //// + + @JvmStatic + internal val ByteSerializerDescriptor = Byte.serializer().descriptor + + @JvmStatic + internal val ShortSerializerDescriptor = Short.serializer().descriptor + + @JvmStatic + internal val IntSerializerDescriptor = Int.serializer().descriptor + + @JvmStatic + internal val LongSerializerDescriptor = Long.serializer().descriptor + + @JvmStatic + internal val FloatSerializerDescriptor = Float.serializer().descriptor + + @JvmStatic + internal val DoubleSerializerDescriptor = Double.serializer().descriptor + + @JvmStatic + internal val CharSerializerDescriptor = Char.serializer().descriptor + + @JvmStatic + internal val BooleanSerializerDescriptor = Boolean.serializer().descriptor + + @JvmStatic + internal val StringSerializerDescriptor = String.serializer().descriptor + + //// endregion BuiltInSerializerConstantsPrimitives CODEGEN //// +} + +@Suppress("UNCHECKED_CAST") +internal fun Setting.valueImplPrimitive(kClass: KClass): SerializerAwareValue? { + return when (kClass) { + //// region Setting_valueImplPrimitive CODEGEN //// + + Byte::class -> byteValueImpl() + Short::class -> shortValueImpl() + Int::class -> intValueImpl() + Long::class -> longValueImpl() + Float::class -> floatValueImpl() + Double::class -> doubleValueImpl() + Char::class -> charValueImpl() + Boolean::class -> booleanValueImpl() + String::class -> stringValueImpl() + + //// endregion Setting_valueImplPrimitive CODEGEN //// + else -> error("Internal error: unexpected type passed: ${kClass.qualifiedName}") + } as SerializerAwareValue? +} + + +//// region Setting_value_PrimitivesImpl CODEGEN //// + +internal fun Setting.valueImpl(default: Byte): SerializerAwareValue { + return object : ByteValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.byteValueImpl(): SerializerAwareValue { + return object : ByteValueImpl() { + override fun onChanged() = this@byteValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Short): SerializerAwareValue { + return object : ShortValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.shortValueImpl(): SerializerAwareValue { + return object : ShortValueImpl() { + override fun onChanged() = this@shortValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Int): SerializerAwareValue { + return object : IntValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.intValueImpl(): SerializerAwareValue { + return object : IntValueImpl() { + override fun onChanged() = this@intValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Long): SerializerAwareValue { + return object : LongValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.longValueImpl(): SerializerAwareValue { + return object : LongValueImpl() { + override fun onChanged() = this@longValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Float): SerializerAwareValue { + return object : FloatValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.floatValueImpl(): SerializerAwareValue { + return object : FloatValueImpl() { + override fun onChanged() = this@floatValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Double): SerializerAwareValue { + return object : DoubleValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.doubleValueImpl(): SerializerAwareValue { + return object : DoubleValueImpl() { + override fun onChanged() = this@doubleValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Char): SerializerAwareValue { + return object : CharValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.charValueImpl(): SerializerAwareValue { + return object : CharValueImpl() { + override fun onChanged() = this@charValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: Boolean): SerializerAwareValue { + return object : BooleanValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.booleanValueImpl(): SerializerAwareValue { + return object : BooleanValueImpl() { + override fun onChanged() = this@booleanValueImpl.onValueChanged(this) + } +} +internal fun Setting.valueImpl(default: String): SerializerAwareValue { + return object : StringValueImpl(default) { + override fun onChanged() = this@valueImpl.onValueChanged(this) + } +} +internal fun Setting.stringValueImpl(): SerializerAwareValue { + return object : StringValueImpl() { + override fun onChanged() = this@stringValueImpl.onValueChanged(this) + } +} + +//// endregion Setting_value_PrimitivesImpl CODEGEN //// diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/asKClass.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/asKClass.kt new file mode 100644 index 000000000..9cf257fc1 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/asKClass.kt @@ -0,0 +1,44 @@ +/* + * 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.setting.internal + +import net.mamoe.mirai.console.command.internal.qualifiedNameOrTip +import net.mamoe.mirai.console.setting.Setting +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.isSubclassOf + +@Suppress("UNCHECKED_CAST") +internal inline fun KType.asKClass(): KClass { + val clazz = requireNotNull(classifier as? KClass) { "Unsupported classifier: $classifier" } + + val fromClass = arguments[0].type?.classifier as? KClass<*> ?: Any::class + val toClass = T::class + + require(toClass.isSubclassOf(fromClass)) { + "Cannot cast KClass<${fromClass.qualifiedNameOrTip}> to KClass<${toClass.qualifiedNameOrTip}>" + } + + return clazz +} + +internal inline fun newSettingInstanceUsingReflection(type: KType): T { + val classifier = type.asKClass() + + return with(classifier) { + objectInstance + ?: createInstanceOrNull() + ?: throw IllegalArgumentException( + "Cannot create Setting instance. " + + "SettingHolder supports Settings implemented as an object " + + "or the ones with a constructor which either has no parameters or all parameters of which are optional, by default newSettingInstance implementation." + ) + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/collectionUtil.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/collectionUtil.kt new file mode 100644 index 000000000..c691f1cd3 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/collectionUtil.kt @@ -0,0 +1,463 @@ +/* + * 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("DuplicatedCode") + +package net.mamoe.mirai.console.setting.internal + +import kotlinx.serialization.ImplicitReflectionSerializer +import kotlinx.serialization.serializer +import net.mamoe.yamlkt.Yaml +import kotlin.reflect.KClass + +// TODO: 2020/6/24 优化性能: 引入一个 comparator 之类来替代将 Int 包装为 Value 后进行 containsKey 比较的方法 + +internal inline fun MutableMap.shadowMap( + crossinline kTransform: (K) -> KR, + crossinline kTransformBack: (KR) -> K, + crossinline vTransform: (V) -> VR, + crossinline vTransformBack: (VR) -> V +): MutableMap { + return object : MutableMap { + override val size: Int get() = this@shadowMap.size + override fun containsKey(key: KR): Boolean = this@shadowMap.containsKey(key.let(kTransformBack)) + override fun containsValue(value: VR): Boolean = this@shadowMap.containsValue(value.let(vTransformBack)) + override fun get(key: KR): VR? = this@shadowMap[key.let(kTransformBack)]?.let(vTransform) + override fun isEmpty(): Boolean = this@shadowMap.isEmpty() + + override val entries: MutableSet> + get() = this@shadowMap.entries.shadowMap( + transform = { entry: MutableMap.MutableEntry -> + object : MutableMap.MutableEntry { + override val key: KR get() = entry.key.let(kTransform) + override val value: VR get() = entry.value.let(vTransform) + override fun setValue(newValue: VR): VR = + entry.setValue(newValue.let(vTransformBack)).let(vTransform) + + override fun hashCode(): Int = 17 * 31 + (key?.hashCode() ?: 0) + (value?.hashCode() ?: 0) + override fun toString(): String = "$key=$value" + override fun equals(other: Any?): Boolean { + if (other == null || other !is Map.Entry<*, *>) return false + return other.key == key && other.value == value + } + } + } as ((MutableMap.MutableEntry) -> MutableMap.MutableEntry), // type inference bug + transformBack = { entry -> + object : MutableMap.MutableEntry { + override val key: K get() = entry.key.let(kTransformBack) + override val value: V get() = entry.value.let(vTransformBack) + override fun setValue(newValue: V): V = + entry.setValue(newValue.let(vTransform)).let(vTransformBack) + + override fun hashCode(): Int = 17 * 31 + (key?.hashCode() ?: 0) + (value?.hashCode() ?: 0) + override fun toString(): String = "$key=$value" + override fun equals(other: Any?): Boolean { + if (other == null || other !is Map.Entry<*, *>) return false + return other.key == key && other.value == value + } + } + } + ) + override val keys: MutableSet + get() = this@shadowMap.keys.shadowMap(kTransform, kTransformBack) + override val values: MutableCollection + get() = this@shadowMap.values.shadowMap(vTransform, vTransformBack) + + override fun clear() = this@shadowMap.clear() + override fun put(key: KR, value: VR): VR? = + this@shadowMap.put(key.let(kTransformBack), value.let(vTransformBack))?.let(vTransform) + + override fun putAll(from: Map) { + from.forEach { (kr, vr) -> + this@shadowMap[kr.let(kTransformBack)] = vr.let(vTransformBack) + } + } + + override fun remove(key: KR): VR? = this@shadowMap.remove(key.let(kTransformBack))?.let(vTransform) + override fun toString(): String = this@shadowMap.toString() + override fun hashCode(): Int = this@shadowMap.hashCode() + } +} + +internal inline fun MutableCollection.shadowMap( + crossinline transform: (E) -> R, + crossinline transformBack: (R) -> E +): MutableCollection { + return object : MutableCollection { + override val size: Int get() = this@shadowMap.size + + override fun contains(element: R): Boolean = this@shadowMap.any { it.let(transform) == element } + override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + override fun isEmpty(): Boolean = this@shadowMap.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@shadowMap.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): R = delegate.next().let(transform) + override fun remove() = delegate.remove() + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun add(element: R): Boolean = this@shadowMap.add(element.let(transformBack)) + + override fun addAll(elements: Collection): Boolean = this@shadowMap.addAll(elements.map(transformBack)) + override fun clear() = this@shadowMap.clear() + + override fun remove(element: R): Boolean = this@shadowMap.removeIf { it.let(transform) == element } + override fun removeAll(elements: Collection): Boolean = elements.all(::remove) + override fun retainAll(elements: Collection): Boolean = this@shadowMap.retainAll(elements.map(transformBack)) + override fun toString(): String = this@shadowMap.toString() + override fun hashCode(): Int = this@shadowMap.hashCode() + } +} + +internal inline fun MutableList.shadowMap( + crossinline transform: (E) -> R, + crossinline transformBack: (R) -> E +): MutableList { + return object : MutableList { + override val size: Int get() = this@shadowMap.size + + override fun contains(element: R): Boolean = this@shadowMap.any { it.let(transform) == element } + override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + override fun get(index: Int): R = this@shadowMap[index].let(transform) + override fun indexOf(element: R): Int = this@shadowMap.indexOfFirst { it.let(transform) == element } + override fun isEmpty(): Boolean = this@shadowMap.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@shadowMap.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): R = delegate.next().let(transform) + override fun remove() = delegate.remove() + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun lastIndexOf(element: R): Int = this@shadowMap.indexOfLast { it.let(transform) == element } + override fun add(element: R): Boolean = this@shadowMap.add(element.let(transformBack)) + override fun add(index: Int, element: R) = this@shadowMap.add(index, element.let(transformBack)) + override fun addAll(index: Int, elements: Collection): Boolean = + this@shadowMap.addAll(index, elements.map(transformBack)) + + override fun addAll(elements: Collection): Boolean = this@shadowMap.addAll(elements.map(transformBack)) + override fun clear() = this@shadowMap.clear() + + override fun listIterator(): MutableListIterator = object : MutableListIterator { + private val delegate = this@shadowMap.listIterator() + override fun hasPrevious(): Boolean = delegate.hasPrevious() + override fun nextIndex(): Int = delegate.nextIndex() + override fun previous(): R = delegate.previous().let(transform) + override fun previousIndex(): Int = delegate.previousIndex() + override fun add(element: R) = delegate.add(element.let(transformBack)) + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): R = delegate.next().let(transform) + override fun remove() = delegate.remove() + override fun set(element: R) = delegate.set(element.let(transformBack)) + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun listIterator(index: Int): MutableListIterator = object : MutableListIterator { + private val delegate = this@shadowMap.listIterator(index) + override fun hasPrevious(): Boolean = delegate.hasPrevious() + override fun nextIndex(): Int = delegate.nextIndex() + override fun previous(): R = delegate.previous().let(transform) + override fun previousIndex(): Int = delegate.previousIndex() + override fun add(element: R) = delegate.add(element.let(transformBack)) + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): R = delegate.next().let(transform) + override fun remove() = delegate.remove() + override fun set(element: R) = delegate.set(element.let(transformBack)) + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun remove(element: R): Boolean = this@shadowMap.removeIf { it.let(transform) == element } + override fun removeAll(elements: Collection): Boolean = elements.all(::remove) + override fun removeAt(index: Int): R = this@shadowMap.removeAt(index).let(transform) + override fun retainAll(elements: Collection): Boolean = this@shadowMap.retainAll(elements.map(transformBack)) + override fun set(index: Int, element: R): R = + this@shadowMap.set(index, element.let(transformBack)).let(transform) + + override fun subList(fromIndex: Int, toIndex: Int): MutableList = + this@shadowMap.subList(fromIndex, toIndex).map(transform).toMutableList() + + override fun toString(): String = this@shadowMap.toString() + override fun hashCode(): Int = this@shadowMap.hashCode() + } +} + + +internal inline fun MutableSet.shadowMap( + crossinline transform: (E) -> R, + crossinline transformBack: (R) -> E +): MutableSet { + return object : MutableSet { + override val size: Int get() = this@shadowMap.size + + override fun contains(element: R): Boolean = this@shadowMap.any { it.let(transform) == element } + override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + override fun isEmpty(): Boolean = this@shadowMap.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@shadowMap.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): R = delegate.next().let(transform) + override fun remove() = delegate.remove() + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun add(element: R): Boolean = this@shadowMap.add(element.let(transformBack)) + override fun addAll(elements: Collection): Boolean = this@shadowMap.addAll(elements.map(transformBack)) + override fun clear() = this@shadowMap.clear() + + override fun remove(element: R): Boolean = this@shadowMap.removeIf { it.let(transform) == element } + override fun removeAll(elements: Collection): Boolean = elements.all(::remove) + override fun retainAll(elements: Collection): Boolean = this@shadowMap.retainAll(elements.map(transformBack)) + override fun toString(): String = this@shadowMap.toString() + override fun hashCode(): Int = this@shadowMap.hashCode() + } +} + +internal inline fun dynamicList(crossinline supplier: () -> List): List { + return object : List { + override val size: Int get() = supplier().size + override fun contains(element: T): Boolean = supplier().contains(element) + override fun containsAll(elements: Collection): Boolean = supplier().containsAll(elements) + override fun get(index: Int): T = supplier()[index] + override fun indexOf(element: T): Int = supplier().indexOf(element) + override fun isEmpty(): Boolean = supplier().isEmpty() + override fun iterator(): Iterator = supplier().iterator() + override fun lastIndexOf(element: T): Int = supplier().lastIndexOf(element) + override fun listIterator(): ListIterator = supplier().listIterator() + override fun listIterator(index: Int): ListIterator = supplier().listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int): List = supplier().subList(fromIndex, toIndex) + override fun toString(): String = supplier().toString() + override fun hashCode(): Int = supplier().hashCode() + } +} + +internal inline fun dynamicSet(crossinline supplier: () -> Set): Set { + return object : Set { + override val size: Int get() = supplier().size + override fun contains(element: T): Boolean = supplier().contains(element) + override fun containsAll(elements: Collection): Boolean = supplier().containsAll(elements) + override fun isEmpty(): Boolean = supplier().isEmpty() + override fun iterator(): Iterator = supplier().iterator() + override fun toString(): String = supplier().toString() + override fun hashCode(): Int = supplier().hashCode() + } +} + + +internal inline fun dynamicMutableList(crossinline supplier: () -> MutableList): MutableList { + return object : MutableList { + override val size: Int get() = supplier().size + override fun contains(element: T): Boolean = supplier().contains(element) + override fun containsAll(elements: Collection): Boolean = supplier().containsAll(elements) + override fun get(index: Int): T = supplier()[index] + override fun indexOf(element: T): Int = supplier().indexOf(element) + override fun isEmpty(): Boolean = supplier().isEmpty() + override fun iterator(): MutableIterator = supplier().iterator() + override fun lastIndexOf(element: T): Int = supplier().lastIndexOf(element) + override fun add(element: T): Boolean = supplier().add(element) + override fun add(index: Int, element: T) = supplier().add(index, element) + override fun addAll(index: Int, elements: Collection): Boolean = supplier().addAll(index, elements) + override fun addAll(elements: Collection): Boolean = supplier().addAll(elements) + override fun clear() = supplier().clear() + override fun listIterator(): MutableListIterator = supplier().listIterator() + override fun listIterator(index: Int): MutableListIterator = supplier().listIterator(index) + override fun remove(element: T): Boolean = supplier().remove(element) + override fun removeAll(elements: Collection): Boolean = supplier().removeAll(elements) + override fun removeAt(index: Int): T = supplier().removeAt(index) + override fun retainAll(elements: Collection): Boolean = supplier().retainAll(elements) + override fun set(index: Int, element: T): T = supplier().set(index, element) + override fun subList(fromIndex: Int, toIndex: Int): MutableList = supplier().subList(fromIndex, toIndex) + override fun toString(): String = supplier().toString() + override fun hashCode(): Int = supplier().hashCode() + } +} + + +internal inline fun dynamicMutableSet(crossinline supplier: () -> MutableSet): MutableSet { + return object : MutableSet { + override val size: Int get() = supplier().size + override fun contains(element: T): Boolean = supplier().contains(element) + override fun containsAll(elements: Collection): Boolean = supplier().containsAll(elements) + override fun isEmpty(): Boolean = supplier().isEmpty() + override fun iterator(): MutableIterator = supplier().iterator() + override fun add(element: T): Boolean = supplier().add(element) + override fun addAll(elements: Collection): Boolean = supplier().addAll(elements) + override fun clear() = supplier().clear() + override fun remove(element: T): Boolean = supplier().remove(element) + override fun removeAll(elements: Collection): Boolean = supplier().removeAll(elements) + override fun retainAll(elements: Collection): Boolean = supplier().retainAll(elements) + override fun toString(): String = supplier().toString() + override fun hashCode(): Int = supplier().hashCode() + } +} + +@Suppress("UNCHECKED_CAST", "USELESS_CAST") // type inference bug +internal inline fun MutableMap.observable(crossinline onChanged: () -> Unit): MutableMap { + return object : MutableMap, Map by (this as Map) { + override val keys: MutableSet + get() = this@observable.keys.observable(onChanged) + override val values: MutableCollection + get() = this@observable.values.observable(onChanged) + override val entries: MutableSet> + get() = this@observable.entries.observable(onChanged) + + override fun clear() = this@observable.clear().also { onChanged() } + override fun put(key: K, value: V): V? = this@observable.put(key, value).also { onChanged() } + override fun putAll(from: Map) = this@observable.putAll(from).also { onChanged() } + override fun remove(key: K): V? = this@observable.remove(key).also { onChanged() } + override fun toString(): String = this@observable.toString() + override fun hashCode(): Int = this@observable.hashCode() + } +} + +internal inline fun MutableList.observable(crossinline onChanged: () -> Unit): MutableList { + return object : MutableList { + override val size: Int get() = this@observable.size + override fun contains(element: T): Boolean = this@observable.contains(element) + override fun containsAll(elements: Collection): Boolean = this@observable.containsAll(elements) + override fun get(index: Int): T = this@observable[index] + override fun indexOf(element: T): Int = this@observable.indexOf(element) + override fun isEmpty(): Boolean = this@observable.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@observable.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): T = delegate.next() + override fun remove() = delegate.remove().also { onChanged() } + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun lastIndexOf(element: T): Int = this@observable.lastIndexOf(element) + override fun add(element: T): Boolean = this@observable.add(element).also { onChanged() } + override fun add(index: Int, element: T) = this@observable.add(index, element).also { onChanged() } + override fun addAll(index: Int, elements: Collection): Boolean = + this@observable.addAll(index, elements).also { onChanged() } + + override fun addAll(elements: Collection): Boolean = this@observable.addAll(elements).also { onChanged() } + override fun clear() = this@observable.clear().also { onChanged() } + override fun listIterator(): MutableListIterator = object : MutableListIterator { + private val delegate = this@observable.listIterator() + override fun hasPrevious(): Boolean = delegate.hasPrevious() + override fun nextIndex(): Int = delegate.nextIndex() + override fun previous(): T = delegate.previous() + override fun previousIndex(): Int = delegate.previousIndex() + override fun add(element: T) = delegate.add(element).also { onChanged() } + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): T = delegate.next() + override fun remove() = delegate.remove().also { onChanged() } + override fun set(element: T) = delegate.set(element).also { onChanged() } + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun listIterator(index: Int): MutableListIterator = object : MutableListIterator { + private val delegate = this@observable.listIterator(index) + override fun hasPrevious(): Boolean = delegate.hasPrevious() + override fun nextIndex(): Int = delegate.nextIndex() + override fun previous(): T = delegate.previous() + override fun previousIndex(): Int = delegate.previousIndex() + override fun add(element: T) = delegate.add(element).also { onChanged() } + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): T = delegate.next() + override fun remove() = delegate.remove().also { onChanged() } + override fun set(element: T) = delegate.set(element).also { onChanged() } + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun remove(element: T): Boolean = this@observable.remove(element).also { onChanged() } + override fun removeAll(elements: Collection): Boolean = + this@observable.removeAll(elements).also { onChanged() } + + override fun removeAt(index: Int): T = this@observable.removeAt(index).also { onChanged() } + override fun retainAll(elements: Collection): Boolean = + this@observable.retainAll(elements).also { onChanged() } + + override fun set(index: Int, element: T): T = this@observable.set(index, element).also { onChanged() } + override fun subList(fromIndex: Int, toIndex: Int): MutableList = this@observable.subList(fromIndex, toIndex) + override fun toString(): String = this@observable.toString() + override fun hashCode(): Int = this@observable.hashCode() + } +} + +internal inline fun MutableCollection.observable(crossinline onChanged: () -> Unit): MutableCollection { + return object : MutableCollection { + override val size: Int get() = this@observable.size + override fun contains(element: T): Boolean = this@observable.contains(element) + override fun containsAll(elements: Collection): Boolean = this@observable.containsAll(elements) + override fun isEmpty(): Boolean = this@observable.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@observable.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): T = delegate.next() + override fun remove() = delegate.remove().also { onChanged() } + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun add(element: T): Boolean = this@observable.add(element).also { onChanged() } + override fun addAll(elements: Collection): Boolean = this@observable.addAll(elements).also { onChanged() } + override fun clear() = this@observable.clear().also { onChanged() } + override fun remove(element: T): Boolean = this@observable.remove(element).also { onChanged() } + override fun removeAll(elements: Collection): Boolean = + this@observable.removeAll(elements).also { onChanged() } + + override fun retainAll(elements: Collection): Boolean = + this@observable.retainAll(elements).also { onChanged() } + + override fun toString(): String = this@observable.toString() + override fun hashCode(): Int = this@observable.hashCode() + } +} + +internal inline fun MutableSet.observable(crossinline onChanged: () -> Unit): MutableSet { + return object : MutableSet { + override val size: Int get() = this@observable.size + override fun contains(element: T): Boolean = this@observable.contains(element) + override fun containsAll(elements: Collection): Boolean = this@observable.containsAll(elements) + override fun isEmpty(): Boolean = this@observable.isEmpty() + override fun iterator(): MutableIterator = object : MutableIterator { + private val delegate = this@observable.iterator() + override fun hasNext(): Boolean = delegate.hasNext() + override fun next(): T = delegate.next() + override fun remove() = delegate.remove().also { onChanged() } + override fun toString(): String = delegate.toString() + override fun hashCode(): Int = delegate.hashCode() + } + + override fun add(element: T): Boolean = this@observable.add(element).also { onChanged() } + override fun addAll(elements: Collection): Boolean = this@observable.addAll(elements).also { onChanged() } + override fun clear() = this@observable.clear().also { onChanged() } + override fun remove(element: T): Boolean = this@observable.remove(element).also { onChanged() } + override fun removeAll(elements: Collection): Boolean = + this@observable.removeAll(elements).also { onChanged() } + + override fun retainAll(elements: Collection): Boolean = + this@observable.retainAll(elements).also { onChanged() } + + override fun toString(): String = this@observable.toString() + override fun hashCode(): Int = this@observable.hashCode() + } +} + + +@OptIn(ImplicitReflectionSerializer::class) +internal fun Any.smartCastPrimitive(clazz: KClass): R { + kotlin.runCatching { + return Yaml.default.parse(clazz.serializer(), this.toString()) + }.getOrElse { + throw IllegalArgumentException("Cannot cast '$this' to ${clazz.qualifiedName}", it) + } +} + diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/serializerUtil.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/serializerUtil.kt new file mode 100644 index 000000000..7887e4052 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/setting/internal/serializerUtil.kt @@ -0,0 +1,43 @@ +/* + * 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.setting.internal + +import kotlinx.serialization.* +import kotlin.reflect.KProperty +import kotlin.reflect.full.findAnnotation + +internal val KProperty<*>.serialNameOrPropertyName: String get() = this.findAnnotation()?.value ?: this.name + +internal fun Int.isOdd() = this and 0b1 != 0 + +internal inline fun KSerializer.bind( + crossinline setter: (E) -> Unit, + crossinline getter: () -> E +): KSerializer { + return object : KSerializer { + override val descriptor: SerialDescriptor get() = this@bind.descriptor + override fun deserialize(decoder: Decoder): E = this@bind.deserialize(decoder).also { setter(it) } + + @Suppress("UNCHECKED_CAST") + override fun serialize(encoder: Encoder, value: E) = + this@bind.serialize(encoder, getter()) + } +} + +internal inline fun KSerializer.map( + crossinline serializer: (R) -> E, + crossinline deserializer: (E) -> R +): KSerializer { + return object : KSerializer { + override val descriptor: SerialDescriptor get() = this@map.descriptor + override fun deserialize(decoder: Decoder): R = this@map.deserialize(decoder).let(deserializer) + override fun serialize(encoder: Encoder, value: R) = this@map.serialize(encoder, value.let(serializer)) + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotManagers.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotManagers.kt new file mode 100644 index 000000000..f30e8f3b3 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotManagers.kt @@ -0,0 +1,65 @@ +/* + * 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:JvmName("BotManagers") + +package net.mamoe.mirai.console.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.setting.* +import net.mamoe.mirai.console.setting.SettingStorage.Companion.load +import net.mamoe.mirai.contact.User +import net.mamoe.mirai.utils.minutesToMillis +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +/** + * 判断此用户是否为 console 管理员 + */ +public val User.isManager: Boolean get() = this.id in this.bot.managers + +public fun Bot.removeManager(id: Long): Boolean { + return ManagersConfig[this].remove(id) +} + +public val Bot.managers: List + get() = ManagersConfig[this].toList() + +internal fun Bot.addManager(id: Long): Boolean { + return ManagersConfig[this].add(id) +} + + +internal object ManagersConfig : Setting by ConsoleBuiltInSettingStorage.load() { + private val managers: MutableMap> by value() + + internal operator fun get(bot: Bot): MutableSet = managers.getOrPut(bot.id, ::mutableSetOf) +} + + +internal fun CoroutineContext.overrideWithSupervisorJob(): CoroutineContext = this + SupervisorJob(this[Job]) +internal fun CoroutineScope.childScope(context: CoroutineContext = EmptyCoroutineContext): CoroutineScope = + CoroutineScope(this.coroutineContext.overrideWithSupervisorJob() + context) + +internal object ConsoleBuiltInSettingHolder : AutoSaveSettingHolder, + CoroutineScope by MiraiConsole.childScope() { + override val autoSaveIntervalMillis: LongRange = 30.minutesToMillis..60.minutesToMillis + override val name: String get() = "ConsoleBuiltIns" +} + +internal object ConsoleBuiltInSettingStorage : + SettingStorage by MiraiConsoleImplementationBridge.settingStorageForJarPluginLoader { + + inline fun load(): T = load(ConsoleBuiltInSettingHolder) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt new file mode 100644 index 000000000..7a48de899 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt @@ -0,0 +1,51 @@ +/* + * 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("INAPPLICABLE_JVM_NAME", "unused") + +package net.mamoe.mirai.console.utils + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.mamoe.mirai.console.MiraiConsole + +/** + * Console 输入. 由于 console 接管了 stdin, [readLine] 等操作需要在这里进行. + */ +public interface ConsoleInput { + /** + * 以 [提示][hint] 向用户索要一个输入 + */ + @JvmSynthetic + public suspend fun requestInput(hint: String): String + + /** + * 以 [提示][hint] 向用户索要一个输入. 仅供 Java 调用者使用 + */ + @JvmName("requestInput") + @JavaFriendlyAPI + public fun requestInputBlocking(hint: String): String + + public companion object INSTANCE : ConsoleInput by ConsoleInputImpl { + public suspend inline fun MiraiConsole.requestInput(hint: String): String = ConsoleInput.requestInput(hint) + } +} + +@Suppress("unused") +internal object ConsoleInputImpl : ConsoleInput { + private val inputLock = Mutex() + + override suspend fun requestInput( + hint: String + ): String = inputLock.withLock { MiraiConsole.frontEnd.requestInput(hint) } + + @JavaFriendlyAPI + override fun requestInputBlocking(hint: String): String = runBlocking { requestInput(hint) } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt new file mode 100644 index 000000000..ea93faeca --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaFriendlyAPI.kt @@ -0,0 +1,48 @@ +/* + * 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.utils + +import kotlin.annotation.AnnotationTarget.* + +/** + * 表明这个 API 是为了让 Java 使用者调用更方便. Kotlin 使用者不应该使用这些 API. + */ +@Retention(AnnotationRetention.SOURCE) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Target(PROPERTY, FUNCTION, TYPE, CLASS) +internal annotation class JavaFriendlyAPI + +/** + * 标记为一个仅供 mirai-console 内部使用的 API. + * + * 这些 API 可能会在任意时刻更改, 且不会发布任何预警. + * 非常不建议在发行版本中使用这些 API. + */ +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR, CLASS, FUNCTION, PROPERTY) +@MustBeDocumented +public annotation class ConsoleInternalAPI( + val message: String = "" +) + +/** + * 标记一个实验性的 API. + * + * 这些 API 不具有稳定性, 且可能会在任意时刻更改. + * 不建议在发行版本中使用这些 API. + */ +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) +@MustBeDocumented +public annotation class ConsoleExperimentalAPI( + val message: String = "" +) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaPluginScheduler.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaPluginScheduler.kt new file mode 100644 index 000000000..3029fb80e --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/JavaPluginScheduler.kt @@ -0,0 +1,95 @@ +/* + * 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.utils + +import kotlinx.coroutines.* +import kotlinx.coroutines.future.future +import net.mamoe.mirai.console.plugin.jvm.JavaPlugin +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future +import kotlin.coroutines.CoroutineContext + + +/** + * 拥有生命周期管理的 Java 线程池. + * + * 在插件被 [卸载][JavaPlugin.onDisable] 时将会自动停止. + * + * @see JavaPlugin.scheduler 获取实例 + */ +public class JavaPluginScheduler internal constructor(parentCoroutineContext: CoroutineContext) : CoroutineScope { + public override val coroutineContext: CoroutineContext = + parentCoroutineContext + SupervisorJob(parentCoroutineContext[Job]) + + /** + * 新增一个 Repeating Task (定时任务) + * + * 这个 Runnable 会被每 [intervalMs] 调用一次(不包含 [runnable] 执行时间) + * + * @see Future.cancel 取消这个任务 + */ + public fun repeating(intervalMs: Long, runnable: Runnable): Future { + return this.future { + while (isActive) { + withContext(Dispatchers.IO) { runnable.run() } + delay(intervalMs) + } + null + } + } + + /** + * 新增一个 Delayed Task (延迟任务) + * + * 在延迟 [delayMillis] 后执行 [runnable] + */ + public fun delayed(delayMillis: Long, runnable: Runnable): CompletableFuture { + return future { + delay(delayMillis) + withContext(Dispatchers.IO) { + runnable.run() + } + null + } + } + + /** + * 新增一个 Delayed Task (延迟任务) + * + * 在延迟 [delayMillis] 后执行 [runnable] + */ + public fun delayed(delayMillis: Long, runnable: Callable): CompletableFuture { + return future { + delay(delayMillis) + withContext(Dispatchers.IO) { runnable.call() } + null + } + } + + /** + * 异步执行一个任务, 最终返回 [Future], 与 Java 使用方法无异, 但效率更高且可以在插件关闭时停止 + */ + public fun async(supplier: Callable): Future { + return future { + withContext(Dispatchers.IO) { supplier.call() } + } + } + + /** + * 异步执行一个任务, 没有返回 + */ + public fun async(runnable: Runnable): Future { + return future { + withContext(Dispatchers.IO) { runnable.run() } + null + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt new file mode 100644 index 000000000..66144d8a9 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ResourceContainer.kt @@ -0,0 +1,68 @@ +/* + * 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") + +package net.mamoe.mirai.console.utils + +import net.mamoe.mirai.console.encodeToString +import net.mamoe.mirai.console.plugin.jvm.JvmPlugin +import net.mamoe.mirai.console.utils.ResourceContainer.Companion.asResourceContainer +import java.io.InputStream +import java.nio.charset.Charset +import kotlin.reflect.KClass + +/** + * 资源容器. + * + * 资源容器可能使用 [Class.getResourceAsStream], 也可能使用其他方式, 取决于实现方式. + * + * @see JvmPlugin [JvmPlugin] 实现 [ResourceContainer], 使用 [ResourceContainer.asResourceContainer] + */ +public interface ResourceContainer { + /** + * 获取一个资源文件 + */ + public fun getResourceAsStream(name: String): InputStream + + /** + * 读取一个资源文件并以 [Charsets.UTF_8] 编码为 [String] + */ + @JvmDefault + public fun getResource(name: String): String = getResource(name, Charsets.UTF_8) + + /** + * 读取一个资源文件并以 [charset] 编码为 [String] + */ + @JvmDefault + public fun getResource(name: String, charset: Charset): String = + this.getResourceAsStream(name).use { it.readBytes() }.encodeToString(charset) + + public companion object { + /** + * 使用 [Class.getResourceAsStream] 读取资源文件 + */ + @JvmStatic + @JvmName("create") + public fun KClass<*>.asResourceContainer(): ResourceContainer = this.java.asResourceContainer() + + /** + * 使用 [Class.getResourceAsStream] 读取资源文件 + */ + @JvmStatic + @JvmName("create") + public fun Class<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this) + } +} + +private class ClassAsResourceContainer( + private val clazz: Class<*> +) : ResourceContainer { + override fun getResourceAsStream(name: String): InputStream = clazz.getResourceAsStream(name) +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/SemverAsStringSerializer.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/SemverAsStringSerializer.kt new file mode 100644 index 000000000..62c3c5a54 --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/SemverAsStringSerializer.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 + */ + +package net.mamoe.mirai.console.utils + +import com.vdurmont.semver4j.Semver +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.console.setting.internal.map + +@Serializer(forClass = Semver::class) +internal object SemverAsStringSerializerLoose : KSerializer by String.serializer().map( + serializer = { it.toString() }, + deserializer = { Semver(it, Semver.SemverType.LOOSE) } +) + +@Serializer(forClass = Semver::class) +internal object SemverAsStringSerializerIvy : KSerializer by String.serializer().map( + serializer = { it.toString() }, + deserializer = { Semver(it, Semver.SemverType.IVY) } +) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/retryCatching.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/retryCatching.kt new file mode 100644 index 000000000..8b2d02e9c --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/retryCatching.kt @@ -0,0 +1,38 @@ +/* + * 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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") + +package net.mamoe.mirai.console.utils + +import org.jetbrains.annotations.Range +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.internal.InlineOnly + +/** + * 执行 [n] 次 [block], 在第一次成功时返回执行结果, 在捕获到异常时返回异常. + */ +@InlineOnly +public inline fun retryCatching(n: @Range(from = 1, to = Int.MAX_VALUE.toLong()) Int, block: () -> R): Result { + contract { + callsInPlace(block, InvocationKind.AT_LEAST_ONCE) + } + require(n >= 1) { "param n for retryCatching must not be negative" } + var exception: Throwable? = null + repeat(n) { + try { + return Result.success(block()) + } catch (e: Throwable) { + exception?.addSuppressed(e) + exception = e + } + } + return Result.failure(exception!!) +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt new file mode 100644 index 000000000..6c918fbcd --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/TestMiraiConosle.kt @@ -0,0 +1,86 @@ +/* + * 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 + +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.plugin.DeferredPluginLoader +import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.setting.MemorySettingStorage +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.utils.DefaultLogger +import net.mamoe.mirai.utils.LoginSolver +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.PlatformLogger +import java.io.File +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.test.assertNotNull + +@OptIn(ConsoleInternalAPI::class) +fun initTestEnvironment() { + object : MiraiConsoleImplementation { + override val rootDir: File = createTempDir() + override val frontEnd: MiraiConsoleFrontEnd = object : MiraiConsoleFrontEnd { + override val name: String get() = "Test" + override val version: String get() = "1.0.0" + override fun loggerFor(identity: String?): MiraiLogger = PlatformLogger(identity) + override fun pushBot(bot: Bot) = println("pushBot: $bot") + override suspend fun requestInput(hint: String): String = readLine()!! + override fun createLoginSolver(): LoginSolver = LoginSolver.Default + } + override val mainLogger: MiraiLogger = DefaultLogger("main") + override val builtInPluginLoaders: List> = listOf(DeferredPluginLoader { JarPluginLoader }) + override val consoleCommandSender: ConsoleCommandSender = object : ConsoleCommandSender() { + override suspend fun sendMessage(message: Message) = println(message) + } + override val settingStorageForJarPluginLoader: SettingStorage get() = MemorySettingStorage() + override val settingStorageForBuiltIns: SettingStorage get() = MemorySettingStorage() + override val coroutineContext: CoroutineContext = SupervisorJob() + }.start() +} + +internal object Testing { + @Volatile + internal var cont: Continuation? = null + + @Suppress("UNCHECKED_CAST") + suspend fun withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R { + @Suppress("RemoveExplicitTypeArguments") // bug + return if (timeout != -1L) { + withTimeout(timeout) { + suspendCancellableCoroutine { ct -> + this@Testing.cont = ct as Continuation + runBlocking { block() } + } + } + } else { + suspendCancellableCoroutine { ct -> + this.cont = ct as Continuation + runBlocking { block() } + } + } + } + + fun ok(result: Any? = Unit) { + val cont = cont + assertNotNull(cont) + cont.resume(result) + } +} 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 new file mode 100644 index 000000000..b26703001 --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommand.kt @@ -0,0 +1,214 @@ +/* + * 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") + +package net.mamoe.mirai.console.command + +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.console.Testing +import net.mamoe.mirai.console.Testing.withTesting +import net.mamoe.mirai.console.command.description.CommandArgParser +import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.internal.InternalCommandManager +import net.mamoe.mirai.console.command.internal.flattenCommandComponents +import net.mamoe.mirai.console.initTestEnvironment +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.toMessage +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import kotlin.test.* + +object TestCompositeCommand : CompositeCommand( + ConsoleCommandOwner, + "testComposite", "tsC" +) { + @SubCommand + fun mute(seconds: Int) { + Testing.ok(seconds) + } +} + + +object TestSimpleCommand : RawCommand(owner, "testSimple", "tsS") { + override suspend fun CommandSender.onCommand(args: Array) { + Testing.ok(args) + } +} + +internal val sender by lazy { ConsoleCommandSender.instance } +internal val owner by lazy { ConsoleCommandOwner } + +internal class TestCommand { + companion object { + @JvmStatic + @BeforeAll + fun init() { + initTestEnvironment() + } + } + + @Test + fun testRegister() { + try { + assertTrue(TestCompositeCommand.register()) + assertFalse(TestCompositeCommand.register()) + + assertEquals(1, ConsoleCommandOwner.registeredCommands.size) + + assertEquals(1, InternalCommandManager.registeredCommands.size) + assertEquals(2, InternalCommandManager.requiredPrefixCommandMap.size) + } finally { + TestCompositeCommand.unregister() + } + } + + @Test + fun testSimpleExecute() = runBlocking { + assertEquals(arrayOf("test").contentToString(), withTesting> { + TestSimpleCommand.execute(sender, "test") + }.contentToString()) + } + + @Test + fun `test flattenCommandArgs`() { + val result = arrayOf("test", image).flattenCommandComponents().toTypedArray() + + assertEquals("test", result[0]) + assertSame(image, result[1]) + + assertEquals(2, result.size) + } + + @Test + fun testSimpleArgsSplitting() = runBlocking { + assertEquals(arrayOf("test", "ttt", "tt").contentToString(), withTesting> { + TestSimpleCommand.execute(sender, "test ttt tt".toMessage()) + }.contentToString()) + } + + val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") + + @Test + fun `PlainText and Image args splitting`() = runBlocking { + val result = withTesting> { + TestSimpleCommand.execute(sender, "test", image, "tt") + } + assertEquals(arrayOf("test", image, "tt").contentToString(), result.contentToString()) + assertSame(image, result[1]) + } + + @Test + fun `test throw Exception`() = runBlocking { + assertEquals(null, sender.executeCommand("")) + } + + @Test + fun `executing command by string command`() = runBlocking { + TestCompositeCommand.register() + val result = withTesting { + assertNotNull(sender.executeCommand("/testComposite", "mute 1")) + } + + assertEquals(1, result) + } + + @Test + fun `composite command executing`() = runBlocking { + assertEquals(1, withTesting { + assertNotNull(TestCompositeCommand.execute(sender, "mute 1")) + }) + } + + @Test + fun `composite sub command resolution conflict`() { + runBlocking { + val composite = object : CompositeCommand( + ConsoleCommandOwner, + "tr" + ) { + @Suppress("UNUSED_PARAMETER") + @SubCommand + fun mute(seconds: Int) { + Testing.ok(1) + } + + @Suppress("UNUSED_PARAMETER") + @SubCommand + fun mute(seconds: Int, arg2: Int) { + Testing.ok(2) + } + } + + assertFailsWith { + composite.register() + } + /* + composite.withRegistration { + assertEquals(1, withTesting { execute(sender, "tr", "mute 123") }) // one args, resolves to mute(Int) + assertEquals(2, withTesting { execute(sender, "tr", "mute 123 123") }) + }*/ + } + } + + @Test + fun `composite sub command parsing`() { + runBlocking { + class MyClass( + val value: Int + ) + + val composite = object : CompositeCommand( + ConsoleCommandOwner, + "test", + overrideContext = CommandParserContext { + add(object : CommandArgParser { + override fun parse(raw: String, sender: CommandSender): MyClass { + return MyClass(raw.toInt()) + } + + override fun parse(raw: SingleMessage, sender: CommandSender): MyClass { + assertSame(image, raw) + return MyClass(2) + } + }) + } + ) { + @SubCommand + fun mute(seconds: MyClass) { + Testing.ok(seconds) + } + } + + composite.withRegistration { + assertEquals(333, withTesting { execute(sender, "mute 333") }.value) + assertEquals(2, withTesting { execute(sender, "mute", image) }.value) + } + } + } + + @Test + fun `test simple command`() { + runBlocking { + + val simple = object : SimpleCommand(owner, "test") { + @Handler + fun onCommand(string: String) { + Testing.ok(string) + } + } + + simple.withRegistration { + assertEquals("xxx", withTesting { simple.execute(sender, "xxx") }) + assertEquals("xxx", withTesting { sender.executeCommand("/test xxx") }) + } + } + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.kt new file mode 100644 index 000000000..79fb5e58f --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/commanTestingUtil.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.command + +inline fun T.withRegistration(block: T.() -> R): R { + this.register() + try { + return block() + } finally { + this.unregister() + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt new file mode 100644 index 000000000..68892668d --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/setting/SettingTest.kt @@ -0,0 +1,139 @@ +/* + * 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.setting + +import kotlinx.serialization.UnstableDefault +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +@OptIn(ConsoleInternalAPI::class) +internal class SettingTest { + + class MySetting : AbstractSetting() { + var int by value(1) + val map by value>() + val map2 by value>>() + + @ConsoleInternalAPI + override fun onValueChanged(value: Value<*>) { + + } + + override fun setStorage(storage: SettingStorage) { + } + } + + @OptIn(UnstableDefault::class) + private val jsonPrettyPrint = Json(JsonConfiguration(prettyPrint = true)) + private val json = Json(JsonConfiguration.Stable) + + @Test + fun testStringify() { + val setting = MySetting() + + var string = json.stringify(setting.updaterSerializer, Unit) + assertEquals("""{"int":1,"map":{},"map2":{}}""", string) + + setting.int = 2 + + string = json.stringify(setting.updaterSerializer, Unit) + assertEquals("""{"int":2,"map":{},"map2":{}}""", string) + } + + @Test + fun testParseUpdate() { + val setting = MySetting() + + assertEquals(1, setting.int) + + json.parse( + setting.updaterSerializer, """ + {"int":3,"map":{},"map2":{}} + """.trimIndent() + ) + + assertEquals(3, setting.int) + } + + @Test + fun testNestedParseUpdate() { + val setting = MySetting() + + fun delegation() = setting.map + + val refBefore = setting.map + fun reference() = refBefore + + assertEquals(mutableMapOf(), delegation()) // delegation + + json.parse( + setting.updaterSerializer, """ + {"int":1,"map":{"t":"test"},"map2":{}} + """.trimIndent() + ) + + assertEquals(mapOf("t" to "test").toString(), delegation().toString()) + assertEquals(mapOf("t" to "test").toString(), reference().toString()) + + assertSame(reference(), delegation()) // check shadowing + } + + @Test + fun testDeepNestedParseUpdate() { + val setting = MySetting() + + fun delegation() = setting.map2 + + val refBefore = setting.map2 + fun reference() = refBefore + + assertEquals(mutableMapOf(), delegation()) // delegation + + json.parse( + setting.updaterSerializer, """ + {"int":1,"map":{},"map2":{"t":{"f":"test"}}} + """.trimIndent() + ) + + assertEquals(mapOf("t" to mapOf("f" to "test")).toString(), delegation().toString()) + assertEquals(mapOf("t" to mapOf("f" to "test")).toString(), reference().toString()) + + assertSame(reference(), delegation()) // check shadowing + } + + @Test + fun testDeepNestedTrackingParseUpdate() { + val setting = MySetting() + + setting.map2["t"] = mutableMapOf() + + fun delegation() = setting.map2["t"]!! + + val refBefore = setting.map2["t"]!! + fun reference() = refBefore + + assertEquals(mutableMapOf(), delegation()) // delegation + + json.parse( + setting.updaterSerializer, """ + {"int":1,"map":{},"map2":{"t":{"f":"test"}}} + """.trimIndent() + ) + + assertEquals(mapOf("f" to "test").toString(), delegation().toString()) + assertEquals(mapOf("f" to "test").toString(), reference().toString()) + + assertSame(reference(), delegation()) // check shadowing + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ad1429f2e..fbdc4feeb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,33 +1,17 @@ @file:Suppress("UnstableApiUsage") - -import kotlin.math.pow - +plugins { + id("com.jfrog.bintray") version Versions.bintray apply false +} tasks.withType(JavaCompile::class.java) { options.encoding = "UTF8" } -buildscript { - repositories { - maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") - maven(url = "https://mirrors.huaweicloud.com/repository/maven") - jcenter() - mavenCentral() - } - - dependencies { - classpath("com.github.jengelman.gradle.plugins:shadow:5.2.0") - classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.Kotlin.stdlib}") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.Kotlin.stdlib}") - classpath("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4") // don"t use any other. - } -} - allprojects { group = "net.mamoe" repositories { + mavenLocal() maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") - maven(url = "https://mirrors.huaweicloud.com/repository/maven") jcenter() mavenCentral() } @@ -35,97 +19,8 @@ allprojects { subprojects { afterEvaluate { - apply(plugin = "com.github.johnrengelman.shadow") - val kotlin = - (this as ExtensionAware).extensions.getByName("kotlin") as? org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension - ?: return@afterEvaluate + apply() - tasks.getByName("shadowJar") { - doLast { - this.outputs.files.forEach { - if (it.nameWithoutExtension.endsWith("-all")) { - val output = File( - it.path.substringBeforeLast(File.separator) + File.separator + it.nameWithoutExtension.substringBeforeLast( - "-all" - ) + "." + it.extension - ) - - println("Renaming to ${output.path}") - if (output.exists()) { - output.delete() - } - - it.renameTo(output) - } - } - } - } - - val githubUpload by tasks.creating { - group = "mirai" - dependsOn(tasks.getByName("shadowJar")) - - doFirst { - timeout.set(java.time.Duration.ofHours(3)) - findLatestFile()?.let { (_, file) -> - val filename = file.name - println("Uploading file $filename") - runCatching { - upload.GitHub.upload( - file, - "https://api.github.com/repos/mamoe/mirai-repo/contents/shadow/${project.name}/$filename", - project - ) - }.exceptionOrNull()?.let { - System.err.println("GitHub Upload failed") - it.printStackTrace() // force show stacktrace - throw it - } - } - } - } - - val cuiCloudUpload by tasks.creating { - group = "mirai" - dependsOn(tasks.getByName("shadowJar")) - - doFirst { - timeout.set(java.time.Duration.ofHours(3)) - findLatestFile()?.let { (_, file) -> - val filename = file.name - println("Uploading file $filename") - runCatching { - upload.CuiCloud.upload( - file, - project - ) - }.exceptionOrNull()?.let { - System.err.println("CuiCloud Upload failed") - it.printStackTrace() // force show stacktrace - throw it - } - } - } - - } + setJavaCompileTarget() } -} - - -fun Project.findLatestFile(): Map.Entry { - return File(projectDir, "build/libs").walk() - .filter { it.isFile } - .onEach { println("all files=$it") } - .filter { it.name.matches(Regex("""${project.name}-[0-9][0-9]*(\.[0-9]*)*.*\.jar""")) } - .onEach { println("matched file: ${it.name}") } - .associateBy { it.nameWithoutExtension.substringAfterLast('-') } - .onEach { println("versions: $it") } - .maxBy { (version, file) -> - version.split('.').let { - if (it.size == 2) it + "0" - else it - }.reversed().foldIndexed(0) { index: Int, acc: Int, s: String -> - acc + 100.0.pow(index).toInt() * (s.toIntOrNull() ?: 0) - } - } ?: error("cannot find any file to upload") -} +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a0b4b2f9b..73fd31b47 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -3,7 +3,10 @@ plugins { } repositories { + mavenLocal() jcenter() + maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") + mavenCentral() } kotlin { @@ -24,4 +27,11 @@ dependencies { api(ktor("client-core", "1.3.2")) api(ktor("client-cio", "1.3.2")) api(ktor("client-json", "1.3.2")) + + compileOnly(gradleApi()) + //compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") + //runtimeOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") + compileOnly("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5") + api("com.github.jengelman.gradle.plugins:shadow:6.0.0") } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt b/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt new file mode 100644 index 000000000..f8a251004 --- /dev/null +++ b/buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt @@ -0,0 +1,129 @@ +/* + * 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("UnstableApiUsage") + +import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.attributes +import java.io.File +import kotlin.math.pow + +class MiraiConsoleBuildPlugin : Plugin { + override fun apply(target: Project) = target.run { + apply() + val ext = target.extensions.getByName("ext") as org.gradle.api.plugins.ExtraPropertiesExtension + + if (tasks.none { it.name == "shadowJar" }) { + return@run + } + + tasks.getByName("shadowJar") { + with(this as ShadowJar) { + archiveFileName.set( + "${target.name}-${target.version}.jar" + ) + manifest { + attributes( + "Manifest-Version" to "1", + "Implementation-Vendor" to "Mamoe Technologies", + "Implementation-Title" to target.name.toString(), + "Implementation-Version" to target.version.toString() + "-" + gitVersion + ) + } + @Suppress("UNCHECKED_CAST") + kotlin.runCatching { + (ext["shadowJar"] as? ShadowJar.() -> Unit)?.invoke(this) + } + } + } + + tasks.create("githubUpload") { + group = "mirai" + dependsOn(tasks.getByName("shadowJar")) + + doFirst { + timeout.set(java.time.Duration.ofHours(3)) + findLatestFile().let { (_, file) -> + val filename = file.name + println("Uploading file $filename") + runCatching { + upload.GitHub.upload( + file, + "https://api.github.com/repos/project-mirai/mirai-repo/contents/shadow/${project.name}/$filename", + project + ) + }.exceptionOrNull()?.let { + System.err.println("GitHub Upload failed") + it.printStackTrace() // force show stacktrace + throw it + } + } + } + } + + tasks.create("cuiCloudUpload") { + group = "mirai" + dependsOn(tasks.getByName("shadowJar")) + + doFirst { + timeout.set(java.time.Duration.ofHours(3)) + findLatestFile().let { (_, file) -> + val filename = file.name + println("Uploading file $filename") + runCatching { + upload.CuiCloud.upload( + file, + project + ) + }.exceptionOrNull()?.let { + System.err.println("CuiCloud Upload failed") + it.printStackTrace() // force show stacktrace + throw it + } + } + } + + } + } +} + +fun Project.findLatestFile(): Map.Entry { + return File(projectDir, "build/libs").walk() + .filter { it.isFile } + .onEach { println("all files=$it") } + .filter { it.name.matches(Regex("""${project.name}-[0-9][0-9]*(\.[0-9]*)*.*\.jar""")) } + .onEach { println("matched file: ${it.name}") } + .associateBy { it.nameWithoutExtension.substringAfterLast('-') } + .onEach { println("versions: $it") } + .maxBy { (version, _) -> + version.split('.').let { + if (it.size == 2) it + "0" + else it + }.reversed().foldIndexed(0) { index: Int, acc: Int, s: String -> + acc + 100.0.pow(index).toInt() * (s.toIntOrNull() ?: 0) + } + } ?: error("cannot find any file to upload") +} + +val gitVersion: String by lazy { + runCatching { + val exec = Runtime.getRuntime().exec("git rev-parse HEAD") + exec.waitFor() + exec.inputStream.readBytes().toString(Charsets.UTF_8).trim().also { + println("Git commit id: $it") + } + }.onFailure { + it.printStackTrace() + return@lazy "UNKNOWN" + }.getOrThrow() +} diff --git a/buildSrc/src/main/kotlin/PublishingHelpers.kt b/buildSrc/src/main/kotlin/PublishingHelpers.kt new file mode 100644 index 000000000..d760a137e --- /dev/null +++ b/buildSrc/src/main/kotlin/PublishingHelpers.kt @@ -0,0 +1,126 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.* +import upload.Bintray +import java.util.* +import kotlin.reflect.KProperty + +/* + * 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 + */ + +/** + * Configures the [bintray][com.jfrog.bintray.gradle.BintrayExtension] extension. + */ +@PublishedApi +internal fun org.gradle.api.Project.`bintray`(configure: com.jfrog.bintray.gradle.BintrayExtension.() -> Unit): Unit = + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("bintray", configure) + +@PublishedApi +internal operator fun RegisteringDomainObjectDelegateProviderWithTypeAndAction.provideDelegate( + receiver: Any?, + property: KProperty<*> +) = ExistingDomainObjectDelegate.of( + delegateProvider.register(property.name, type.java, action) +) + +@PublishedApi +internal val org.gradle.api.Project.`sourceSets`: org.gradle.api.tasks.SourceSetContainer + get() = + (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("sourceSets") as org.gradle.api.tasks.SourceSetContainer + +@PublishedApi +internal operator fun ExistingDomainObjectDelegate.getValue(receiver: Any?, property: KProperty<*>): T = + delegate + +/** + * Configures the [publishing][org.gradle.api.publish.PublishingExtension] extension. + */ +@PublishedApi +internal fun org.gradle.api.Project.`publishing`(configure: org.gradle.api.publish.PublishingExtension.() -> Unit): Unit = + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("publishing", configure) + + +inline fun Project.setupPublishing( + artifactId: String, + bintrayRepo: String = "mirai", + bintrayPkgName: String = "mirai-console", + vcs: String = "https://github.com/mamoe/mirai-console" +) { + + tasks.register("ensureBintrayAvailable") { + doLast { + if (!Bintray.isBintrayAvailable(project)) { + error("bintray isn't available. ") + } + } + } + + if (Bintray.isBintrayAvailable(project)) { + bintray { + val keyProps = Properties() + val keyFile = file("../keys.properties") + if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) } + if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) } + + user = Bintray.getUser(project) + key = Bintray.getKey(project) + setPublications("mavenJava") + setConfigurations("archives") + + pkg.apply { + repo = bintrayRepo + name = bintrayPkgName + setLicenses("AGPLv3") + publicDownloadNumbers = true + vcsUrl = vcs + } + } + + @Suppress("DEPRECATION") + val sourcesJar by tasks.registering(Jar::class) { + classifier = "sources" + from(sourceSets["main"].allSource) + } + + publishing { + /* + repositories { + maven { + // change to point to your repo, e.g. http://my.org/repo + url = uri("$buildDir/repo") + } + }*/ + publications { + register("mavenJava", MavenPublication::class) { + from(components["java"]) + + groupId = rootProject.group.toString() + this.artifactId = artifactId + version = version + + pom.withXml { + val root = asNode() + root.appendNode("description", description) + root.appendNode("name", project.name) + root.appendNode("url", vcs) + root.children().last() + } + + artifact(sourcesJar.get()) + } + } + } + } else println("bintray isn't available. NO PUBLICATIONS WILL BE SET") + +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/SetCompileTargetPlugin.kt b/buildSrc/src/main/kotlin/SetCompileTargetPlugin.kt new file mode 100644 index 000000000..8910359a3 --- /dev/null +++ b/buildSrc/src/main/kotlin/SetCompileTargetPlugin.kt @@ -0,0 +1,45 @@ +/* + * 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 + */ + +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.compile.JavaCompile +import java.lang.reflect.Method +import kotlin.reflect.KClass + + +fun Any.reflectMethod(name: String, vararg params: KClass): Pair { + return this to this::class.java.getMethod(name, *params.map { it.java }.toTypedArray()) +} + +operator fun Pair.invoke(vararg args: Any?): Any? { + return second.invoke(first, *args) +} + +@Suppress("NOTHING_TO_INLINE") // or error +fun Project.setJavaCompileTarget() { + tasks.filter { it.name in arrayOf("compileKotlin", "compileTestKotlin") }.forEach { task -> + task + .reflectMethod("getKotlinOptions")()!! + .reflectMethod("setJvmTarget", String::class)("1.8") + } + + + kotlin.runCatching { // apply only when java plugin is available + (extensions.getByName("java") as JavaPluginExtension).run { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + tasks.withType(JavaCompile::class.java) { + options.encoding = "UTF8" + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 000000000..51c506b74 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.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 + */ + +object Versions { + const val core = "1.1.3" + const val console = "1.0-M1" + const val consoleGraphical = "0.0.7" + const val consoleTerminal = "0.1.0" + const val consolePure = console + + const val kotlinCompiler = "1.4.0-rc" // for public explict API + const val kotlinStdlib = "1.4.0-rc" // for not overriding dependant's stdlib dependency + + const val coroutines = "1.3.8-1.4.0-rc" + const val collectionsImmutable = "0.3.2" + const val serialization = "1.0-M1-1.4.0-rc" + const val ktor = "1.3.2-1.4.0-rc" + + const val androidGradle = "3.6.2" + + const val bintray = "1.8.5" +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dependencyExtensions.kt b/buildSrc/src/main/kotlin/dependencyExtensions.kt new file mode 100644 index 000000000..5a9f08d92 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencyExtensions.kt @@ -0,0 +1,14 @@ +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.kotlin.dsl.DependencyHandlerScope + +@Suppress("unused") +fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version" + +@Suppress("unused") +fun DependencyHandlerScope.ktor(id: String, version: String = Versions.ktor) = "io.ktor:ktor-$id:$version" + +@Suppress("unused") +fun DependencyHandler.compileAndRuntime(any: Any) { + add("compileOnly", any) + add("runtimeOnly", any) +} diff --git a/buildSrc/src/main/kotlin/upload/GitHub.kt b/buildSrc/src/main/kotlin/upload/GitHub.kt index 486247e33..b84f84a23 100644 --- a/buildSrc/src/main/kotlin/upload/GitHub.kt +++ b/buildSrc/src/main/kotlin/upload/GitHub.kt @@ -94,13 +94,13 @@ object GitHub { /* * 只能获取1M以内/branch为master的sha * */ - class TargetTooLargeException() : Exception("Target TOO Large") + class TargetTooLargeException : Exception("Target TOO Large") suspend fun getShaSmart(repo: String, filePath: String, project: Project): String? { return withContext(Dispatchers.IO) { val response = Jsoup .connect( - "https://api.github.com/repos/mamoe/$repo/contents/$filePath?access_token=" + getGithubToken( + "https://api.github.com/repos/project-mirai/$repo/contents/$filePath?access_token=" + getGithubToken( project ) ) @@ -129,7 +129,7 @@ object GitHub { val resp = withContext(Dispatchers.IO) { Jsoup .connect( - "https://api.github.com/repos/mamoe/$repo/git/ref/heads/$branch?access_token=" + getGithubToken( + "https://api.github.com/repos/project-mirai/$repo/git/ref/heads/$branch?access_token=" + getGithubToken( project ) ) diff --git a/frontend/mirai-android b/frontend/mirai-android new file mode 160000 index 000000000..9784f7cd3 --- /dev/null +++ b/frontend/mirai-android @@ -0,0 +1 @@ +Subproject commit 9784f7cd3881c64c85c2c191e40f18d9ecf9e40b diff --git a/mirai-console-graphical/README.md b/frontend/mirai-console-graphical/README.md similarity index 100% rename from mirai-console-graphical/README.md rename to frontend/mirai-console-graphical/README.md diff --git a/mirai-console-graphical/build.gradle.kts b/frontend/mirai-console-graphical/build.gradle.kts similarity index 81% rename from mirai-console-graphical/build.gradle.kts rename to frontend/mirai-console-graphical/build.gradle.kts index c9e6de907..12bbba3b6 100644 --- a/mirai-console-graphical/build.gradle.kts +++ b/frontend/mirai-console-graphical/build.gradle.kts @@ -29,17 +29,17 @@ version = Versions.Mirai.consoleGraphical description = "Graphical frontend for mirai-console" dependencies { - compileOnly("net.mamoe:mirai-core:${Versions.Mirai.core}") + compileOnly("net.mamoe:mirai-core:${Versions.core}") implementation(project(":mirai-console")) api(group = "no.tornado", name = "tornadofx", version = "1.7.19") api(group = "com.jfoenix", name = "jfoenix", version = "9.0.8") testApi(project(":mirai-console")) - testApi(kotlinx("coroutines-core", Versions.Kotlin.coroutines)) + testApi(kotlinx("coroutines-core", Versions.coroutines)) testApi(group = "org.yaml", name = "snakeyaml", version = "1.25") - testApi("net.mamoe:mirai-core:${Versions.Mirai.core}") - testApi("net.mamoe:mirai-core-qqandroid:${Versions.Mirai.core}") + testApi("net.mamoe:mirai-core:${Versions.core}") + testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") } kotlin { @@ -47,7 +47,7 @@ kotlin { all { languageSettings.useExperimentalAnnotation("kotlin.Experimental") - languageSettings.useExperimentalAnnotation("kotlin.OptIn") + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") languageSettings.progressiveMode = true languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") } diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt similarity index 91% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt index 46c2b5454..529a94d4b 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiConsoleGraphicalLoader.kt @@ -8,8 +8,7 @@ */ package net.mamoe.mirai.console.graphical -import net.mamoe.mirai.console.pure.MiraiConsoleUIPure - +import kotlinx.coroutines.cancel import net.mamoe.mirai.console.MiraiConsole import tornadofx.launch import kotlin.concurrent.thread @@ -26,7 +25,7 @@ class MiraiConsoleGraphicalLoader { this.coreVersion = coreVersion this.consoleVersion = consoleVersion Runtime.getRuntime().addShutdownHook(thread(start = false) { - MiraiConsole.stop() + MiraiConsole.cancel() }) launch() } diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt similarity index 83% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt index b12f55ce2..e3a336c48 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt @@ -10,7 +10,7 @@ package net.mamoe.mirai.console.graphical import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.stylesheet.PrimaryStyleSheet import net.mamoe.mirai.console.graphical.view.Decorator import tornadofx.App @@ -28,7 +28,11 @@ class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) { override fun init() { super.init() - MiraiConsole.start(find(),MiraiConsoleGraphicalLoader.coreVersion,MiraiConsoleGraphicalLoader.consoleVersion) + MiraiConsole.start( + find(), + MiraiConsoleGraphicalLoader.coreVersion, + MiraiConsoleGraphicalLoader.consoleVersion + ) } override fun stop() { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalFrontEndController.kt similarity index 86% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalFrontEndController.kt index 7b77dcf65..2d09d3c3f 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalFrontEndController.kt @@ -16,16 +16,25 @@ import net.mamoe.mirai.console.graphical.model.* import net.mamoe.mirai.console.graphical.view.dialog.InputDialog import net.mamoe.mirai.console.graphical.view.dialog.VerificationCodeFragment import net.mamoe.mirai.console.plugins.PluginManager -import net.mamoe.mirai.console.utils.MiraiConsoleUI +import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd import net.mamoe.mirai.network.CustomLoginFailedException import net.mamoe.mirai.utils.LoginSolver +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.SimpleLogger import net.mamoe.mirai.utils.SimpleLogger.LogPriority -import tornadofx.* +import tornadofx.Controller +import tornadofx.Scope +import tornadofx.find +import tornadofx.observableListOf import java.text.SimpleDateFormat import java.util.* +import kotlin.collections.List +import kotlin.collections.forEach +import kotlin.collections.mutableMapOf +import kotlin.collections.set import kotlin.coroutines.resume -class MiraiGraphicalUIController : Controller(), MiraiConsoleUI { +class MiraiGraphicalFrontEndController : Controller(), MiraiConsoleFrontEnd { private val settingModel = find() private val loginSolver = GraphicalLoginSolver() @@ -38,7 +47,7 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI { private val consoleInfo = ConsoleInfo() - private val sdf by lazy { SimpleDateFormat("HH:mm:ss") } + internal val sdf by lazy { SimpleDateFormat("HH:mm:ss") } init { // 监听插件重载事件,以重新从console获取插件列表 @@ -65,27 +74,23 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI { fun sendCommand(command: String) = runCommand(ConsoleCommandSender, command) - override fun pushLog(identity: Long, message: String) = Platform.runLater { - this.pushLog(LogPriority.INFO, "", identity, message) - } - // 修改interface之后用来暂时占位 - override fun pushLog(priority: LogPriority, identityStr: String, identity: Long, message: String) { + private val mainLogger = SimpleLogger(null) { priority: LogPriority, message: String?, e: Throwable? -> Platform.runLater { - val time = sdf.format(Date()) - - if (identity == 0L) { - mainLog - } else { - cache[identity]?.logHistory - }?.apply { - add("[$time] $identityStr $message" to priority.name) + mainLog.apply { + add("[$time] $message" to priority.name) trim() } } } + override fun loggerFor(identity: Long): MiraiLogger { + return if (identity == 0L) return mainLogger + else cache[identity]?.logger ?: kotlin.error("bot not found: $identity") + } + + override fun prePushBot(identity: Long) = Platform.runLater { if (!cache.containsKey(identity)) { BotModel(identity).also { @@ -123,13 +128,6 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI { private fun getPluginsFromConsole(): ObservableList = PluginManager.getAllPluginDescriptions().map(::PluginModel).toObservable() - - private fun ObservableList<*>.trim() { - while (size > settingModel.item.maxLongNum) { - this.removeAt(0) - } - } - fun checkUpdate(plugin: PluginModel) { pluginList.forEach { if (it.name == plugin.name && it.author == plugin.author) { @@ -153,6 +151,12 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI { return false } + internal fun ObservableList<*>.trim() { + while (size > settingModel.item.maxLongNum) { + this.removeAt(0) + } + } + fun reloadPlugins() { with(PluginManager) { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/event/ReloadEvent.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/event/ReloadEvent.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/event/ReloadEvent.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/event/ReloadEvent.kt diff --git a/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt new file mode 100644 index 000000000..1ee6aba9b --- /dev/null +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt @@ -0,0 +1,36 @@ +package net.mamoe.mirai.console.graphical.model + +import javafx.beans.property.SimpleObjectProperty +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController +import net.mamoe.mirai.utils.SimpleLogger +import tornadofx.* +import java.util.* + +class BotModel(val uin: Long) { + val botProperty = SimpleObjectProperty(null) + var bot: Bot by botProperty + + val logHistory = observableListOf>() + val logger: SimpleLogger = + SimpleLogger(uin.toString()) { priority: SimpleLogger.LogPriority, message: String?, e: Throwable? -> + + val frontend = find() + + frontend.run { + logHistory.apply { + val time = sdf.format(Date()) + add("[$time] $uin $message" to priority.name) + trim() + } + } + } + + val admins = observableListOf() +} + +class BotViewModel(botModel: BotModel? = null) : ItemViewModel(botModel) { + val bot = bind(BotModel::botProperty) + val logHistory = bind(BotModel::logHistory) + val admins = bind(BotModel::admins) +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/GlobalSetting.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/GlobalSetting.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/GlobalSetting.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/GlobalSetting.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/BaseStyleSheet.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/BaseStyleSheet.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/BaseStyleSheet.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/BaseStyleSheet.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt similarity index 93% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt index fec8b8757..8f84a9334 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/LoginViewStyleSheet.kt @@ -5,7 +5,10 @@ import javafx.scene.effect.BlurType import javafx.scene.effect.DropShadow import javafx.scene.paint.Color import javafx.scene.text.FontWeight -import tornadofx.* +import tornadofx.box +import tornadofx.c +import tornadofx.csselement +import tornadofx.px class LoginViewStyleSheet : BaseStyleSheet() { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PluginViewStyleSheet.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PluginViewStyleSheet.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PluginViewStyleSheet.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PluginViewStyleSheet.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt similarity index 98% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt index 9bb6628e5..29c83094e 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/stylesheet/PrimaryStyleSheet.kt @@ -1,9 +1,11 @@ package net.mamoe.mirai.console.graphical.stylesheet import javafx.scene.Cursor -import javafx.scene.paint.Color import javafx.scene.text.FontWeight -import tornadofx.* +import tornadofx.box +import tornadofx.c +import tornadofx.cssclass +import tornadofx.px class PrimaryStyleSheet : BaseStyleSheet() { companion object { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt similarity index 98% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt index 987df669f..b22023376 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt @@ -6,7 +6,6 @@ import javafx.beans.value.ObservableValue import javafx.collections.ObservableList import javafx.event.EventTarget import javafx.scene.Node -import javafx.scene.control.Button import javafx.scene.control.ListView import tornadofx.SortedFilteredList import tornadofx.attachTo diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/SVG.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/SVG.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/SVG.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/SVG.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/controls.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/controls.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/controls.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/controls.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt similarity index 94% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt index 4d79d55d8..6ed16c396 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt @@ -4,7 +4,7 @@ import javafx.beans.property.SimpleStringProperty import javafx.geometry.Pos import javafx.scene.image.Image import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.stylesheet.LoginViewStyleSheet import net.mamoe.mirai.console.graphical.util.jfxButton import net.mamoe.mirai.console.graphical.util.jfxPasswordfield @@ -13,7 +13,7 @@ import tornadofx.* class LoginView : View("CNM") { - private val controller = find() + private val controller = find() private val qq = SimpleStringProperty("") private val psd = SimpleStringProperty("") diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt similarity index 98% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt index b5d84174b..9d1679e35 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsCenterView.kt @@ -8,7 +8,7 @@ import javafx.scene.control.Button import javafx.scene.control.TreeTableCell import kotlinx.coroutines.runBlocking import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.event.ReloadEvent import net.mamoe.mirai.console.graphical.model.PluginModel import net.mamoe.mirai.console.graphical.stylesheet.PluginViewStyleSheet @@ -19,7 +19,7 @@ import tornadofx.* class PluginsCenterView : View() { - private val controller = find() + private val controller = find() private val center get() = MiraiConsole.frontEnd.pluginCenter private val plugins: ObservableList = observableListOf() diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt similarity index 95% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt index 4d3e335d0..358388bb6 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt @@ -2,8 +2,7 @@ package net.mamoe.mirai.console.graphical.view import com.jfoenix.controls.JFXTreeTableColumn import javafx.scene.control.TreeTableCell -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController -import net.mamoe.mirai.console.graphical.event.ReloadEvent +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.model.PluginModel import net.mamoe.mirai.console.graphical.stylesheet.PluginViewStyleSheet import net.mamoe.mirai.console.graphical.util.jfxButton @@ -14,7 +13,7 @@ import tornadofx.visibleWhen class PluginsView : View() { - private val controller = find() + private val controller = find() val plugins = controller.pluginList override val root = jfxTreeTableView(plugins) { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt similarity index 95% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt index 76f1bd2d1..d5b4658df 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt @@ -3,7 +3,6 @@ package net.mamoe.mirai.console.graphical.view import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXListCell import javafx.collections.ObservableList -import javafx.geometry.Insets import javafx.geometry.Pos import javafx.scene.control.Alert import javafx.scene.control.ButtonType @@ -15,18 +14,14 @@ import javafx.scene.input.KeyCode import javafx.scene.layout.Priority import javafx.stage.FileChooser import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.model.BotModel import net.mamoe.mirai.console.graphical.util.* -import net.mamoe.mirai.console.graphical.util.jfxButton -import net.mamoe.mirai.console.graphical.util.jfxListView -import net.mamoe.mirai.console.graphical.util.jfxTabPane import tornadofx.* -import tornadofx.Stylesheet.Companion.contextMenu class PrimaryView : View() { - private val controller = find() + private val controller = find() private lateinit var mainTabPane: TabPane override val root = borderpane { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt similarity index 95% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt index ac5c5de1f..0fb03be12 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt +++ b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt @@ -1,7 +1,7 @@ package net.mamoe.mirai.console.graphical.view import javafx.geometry.Pos -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalFrontEndController import net.mamoe.mirai.console.graphical.model.GlobalSettingModel import net.mamoe.mirai.console.graphical.util.jfxButton import net.mamoe.mirai.console.graphical.util.jfxTextfield @@ -12,7 +12,7 @@ import java.io.File class SettingsView : View() { - private val controller = find() + private val controller = find() private val settingModel = find() override val root = vbox { diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/InputDialog.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/InputDialog.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/InputDialog.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/InputDialog.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/PluginDetailFragment.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/PluginDetailFragment.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/PluginDetailFragment.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/PluginDetailFragment.kt diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/VerificationCodeFragment.kt b/frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/VerificationCodeFragment.kt similarity index 100% rename from mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/VerificationCodeFragment.kt rename to frontend/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/dialog/VerificationCodeFragment.kt diff --git a/mirai-console-graphical/src/main/resources/character.png b/frontend/mirai-console-graphical/src/main/resources/character.png similarity index 100% rename from mirai-console-graphical/src/main/resources/character.png rename to frontend/mirai-console-graphical/src/main/resources/character.png diff --git a/mirai-console-graphical/src/main/resources/logo.png b/frontend/mirai-console-graphical/src/main/resources/logo.png similarity index 100% rename from mirai-console-graphical/src/main/resources/logo.png rename to frontend/mirai-console-graphical/src/main/resources/logo.png diff --git a/mirai-console-graphical/src/test/kotlin/Main.kt b/frontend/mirai-console-graphical/src/test/kotlin/Main.kt similarity index 100% rename from mirai-console-graphical/src/test/kotlin/Main.kt rename to frontend/mirai-console-graphical/src/test/kotlin/Main.kt diff --git a/frontend/mirai-console-pure/build.gradle.kts b/frontend/mirai-console-pure/build.gradle.kts new file mode 100644 index 000000000..7480b30fd --- /dev/null +++ b/frontend/mirai-console-pure/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + kotlin("jvm") version Versions.kotlinCompiler + kotlin("plugin.serialization") version Versions.kotlinCompiler + id("java") + `maven-publish` + id("com.jfrog.bintray") +} + +kotlin { + sourceSets { + all { + languageSettings.enableLanguageFeature("InlineClasses") + + languageSettings.useExperimentalAnnotation("kotlin.Experimental") + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + languageSettings.progressiveMode = true + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiExperimentalAPI") + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.console.utils.ConsoleExperimentalAPI") + languageSettings.useExperimentalAnnotation("net.mamoe.mirai.console.ConsoleFrontEndImplementation") + languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") + languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") + languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + } + } +} + +dependencies { + implementation("org.jline:jline:3.15.0") + implementation("org.fusesource.jansi:jansi:1.18") + + compileAndRuntime(project(":mirai-console")) + compileAndRuntime("net.mamoe:mirai-core:${Versions.core}") + compileAndRuntime(kotlin("stdlib", Versions.kotlinStdlib)) // embedded by core + + runtimeOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}") + testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") + testApi(project(":mirai-console")) +} + +ext.apply { + // 傻逼 compileAndRuntime 没 exclude 掉 + // 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置 + val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = { + dependencyFilter.include { + when ("${it.moduleGroup}:${it.moduleName}") { + "org.jline:jline" -> true + "org.fusesource.jansi:jansi" -> true + else -> false + } + } + } + this.set("shadowJar", x) +} + +version = Versions.consolePure + +description = "Console Pure CLI frontend for mirai" + +setupPublishing("mirai-console-pure", bintrayPkgName = "mirai-console-pure") + +// endregion \ No newline at end of file diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt new file mode 100644 index 000000000..744a062c0 --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/BufferedOutputStream.kt @@ -0,0 +1,85 @@ +/* + * 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.pure + +import java.io.ByteArrayOutputStream +import java.io.OutputStream + +private const val LN = 10.toByte() + +internal class BufferedOutputStream @JvmOverloads constructor( + private val size: Int = 1024 * 1024 * 1024, + private val logger: (String?) -> Unit +) : ByteArrayOutputStream(size + 1) { + override fun write(b: Int) { + if (this.count >= size) { + flush() + } + if (b == 10) { + flush() + } else { + super.write(b) + } + } + + override fun write(b: ByteArray) { + write(b, 0, b.size) + } + + private fun ByteArray.findSplitter(off: Int, end: Int): Int { + var o = off + while (o < end) { + if (get(o) == LN) { + return o + } + o++ + } + return -1 + } + + override fun write(b: ByteArray, off: Int, len: Int) { + val ed = off + len + if (ed > b.size || ed < 0) { + throw ArrayIndexOutOfBoundsException() + } + write0(b, off, ed) + } + + private fun write0(b: ByteArray, off: Int, end: Int) { + val size = end - off + if (size < 1) return + val spl = b.findSplitter(off, end) + if (spl == -1) { + val over = this.size - (size + count) + if (over < 0) { + // cutting + write0(b, off, end + over) + flush() + write0(b, off - over, end) + } else { + super.write(b, off, size) + } + } else { + write0(b, off, spl) + flush() + write0(b, spl + 1, end) + } + } + + override fun writeTo(out: OutputStream?) { + throw UnsupportedOperationException() + } + + override fun flush() { + logger(String(buf, 0, count, Charsets.UTF_8)) + count = 0 + } +} diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleUtils.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleUtils.kt new file mode 100644 index 000000000..c9502a4f3 --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleUtils.kt @@ -0,0 +1,48 @@ +/* + * 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.pure + +import org.jline.reader.LineReader +import org.jline.reader.LineReaderBuilder +import org.jline.reader.impl.completer.NullCompleter +import org.jline.terminal.Terminal +import org.jline.terminal.TerminalBuilder + +internal object ConsoleUtils { + + val lineReader: LineReader + val terminal: Terminal + + init { + + val dumb = System.getProperty("java.class.path") + .contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null || System.getenv("mirai.idea") !== null + + terminal = kotlin.runCatching { + TerminalBuilder.builder() + .dumb(dumb) + .build() + }.recoverCatching { + TerminalBuilder.builder() + .jansi(true) + .build() + }.recoverCatching { + TerminalBuilder.builder() + .system(true) + .build() + }.getOrThrow() + + lineReader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(NullCompleter()) + .build() + } +} \ No newline at end of file diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt new file mode 100644 index 000000000..55ac9bbec --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleFrontEndPure.kt @@ -0,0 +1,200 @@ +/* + * 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( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "CANNOT_OVERRIDE_INVISIBLE_MEMBER", + "INVISIBLE_SETTER", + "INVISIBLE_GETTER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING", + "EXPOSED_SUPER_CLASS" +) + +package net.mamoe.mirai.console.pure + +//import net.mamoe.mirai.console.command.CommandManager +//import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.mamoe.mirai.Bot +import net.mamoe.mirai.console.MiraiConsoleBuildConstants +import net.mamoe.mirai.console.MiraiConsoleFrontEnd +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.utils.DefaultLoginSolver +import net.mamoe.mirai.utils.LoginSolver +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.PlatformLogger +import org.fusesource.jansi.Ansi +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +private val ANSI_RESET = Ansi().reset().toString() + +internal val LoggerCreator: (identity: String?) -> MiraiLogger = { + PlatformLogger(identity = it, output = { line -> + ConsoleUtils.lineReader.printAbove(line + ANSI_RESET) + }) +} + +/** + * mirai-console-pure 前端实现 + * + * @see MiraiConsoleImplementationPure 后端实现 + * @see MiraiConsolePureLoader CLI 入口点 + */ +@ConsoleInternalAPI +@Suppress("unused") +object MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd { + private val globalLogger = LoggerCreator("Mirai") + private val cachedLoggers = ConcurrentHashMap() + + // companion object { + // ANSI color codes + const val COLOR_RED = "\u001b[38;5;196m" + const val COLOR_CYAN = "\u001b[38;5;87m" + const val COLOR_GREEN = "\u001b[38;5;82m" + + // use a dark yellow(more like orange) instead of light one to save Solarized-light users + const val COLOR_YELLOW = "\u001b[38;5;220m" + const val COLOR_GREY = "\u001b[38;5;244m" + const val COLOR_BLUE = "\u001b[38;5;27m" + const val COLOR_NAVY = "\u001b[38;5;24m" // navy uniform blue + const val COLOR_PINK = "\u001b[38;5;207m" + const val COLOR_RESET = "\u001b[39;49m" + // } + + val sdf by lazy { + SimpleDateFormat("HH:mm:ss") + } + override val name: String + get() = "Pure" + override val version: String + get() = MiraiConsoleBuildConstants.version + + override fun loggerFor(identity: String?): MiraiLogger { + identity?.apply { + return cachedLoggers.computeIfAbsent(this, LoggerCreator) + } + return globalLogger + } + + override fun pushBot(bot: Bot) { + } + + override suspend fun requestInput(hint: String): String { + if (hint.isNotEmpty()) { + ConsoleUtils.lineReader.printAbove( + Ansi.ansi() + .fgCyan().a(sdf.format(Date())) + .fgMagenta().a(hint) + .toString() + ) + } + return withContext(Dispatchers.IO) { + ConsoleUtils.lineReader.readLine("> ") + } + } + + override fun createLoginSolver(): LoginSolver { + return DefaultLoginSolver( + input = suspend { + requestInput("") + } + ) + } +} + +/* +class MiraiConsoleFrontEndPure : MiraiConsoleFrontEnd { + private var requesting = false + private var requestStr = "" + + @Suppress("unused") + companion object { + // ANSI color codes + const val COLOR_RED = "\u001b[38;5;196m" + const val COLOR_CYAN = "\u001b[38;5;87m" + const val COLOR_GREEN = "\u001b[38;5;82m" + + // use a dark yellow(more like orange) instead of light one to save Solarized-light users + const val COLOR_YELLOW = "\u001b[38;5;220m" + const val COLOR_GREY = "\u001b[38;5;244m" + const val COLOR_BLUE = "\u001b[38;5;27m" + const val COLOR_NAVY = "\u001b[38;5;24m" // navy uniform blue + const val COLOR_PINK = "\u001b[38;5;207m" + const val COLOR_RESET = "\u001b[39;49m" + } + + init { + thread(name = "Mirai Console Input Thread") { + while (true) { + val input = readLine() ?: return@thread + if (requesting) { + requestStr = input + requesting = false + } else { + CommandManager.runCommand(ConsoleCommandSender, input) + } + } + } + } + + val sdf by lazy { + SimpleDateFormat("HH:mm:ss") + } + + override val logger: MiraiLogger = DefaultLogger("Console") // CLI logger from mirai-core + + fun pushLog(identity: Long, message: String) { + println("\u001b[0m " + sdf.format(Date()) + " $message") + } + + override fun prePushBot(identity: Long) { + + } + + override fun pushBot(bot: Bot) { + + } + + override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) { + + } + + override suspend fun requestInput(hint: String): String { + if (hint.isNotEmpty()) { + println("\u001b[0m " + sdf.format(Date()) + COLOR_PINK + " $hint") + } + requesting = true + while (true) { + delay(50) + if (!requesting) { + return requestStr + } + } + } + + override fun pushBotAdminStatus(identity: Long, admins: List) { + + } + + override fun createLoginSolver(): LoginSolver { + return DefaultLoginSolver( + input = suspend { + requestInput("") + } + ) + } + +} + +*/ 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 new file mode 100644 index 000000000..75c8b4f3f --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt @@ -0,0 +1,65 @@ +/* + * 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( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "CANNOT_OVERRIDE_INVISIBLE_MEMBER", + "INVISIBLE_SETTER", + "INVISIBLE_GETTER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING", + "EXPOSED_SUPER_CLASS" +) +@file:OptIn(ConsoleInternalAPI::class, ConsoleFrontEndImplementation::class) + +package net.mamoe.mirai.console.pure + + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import net.mamoe.mirai.console.ConsoleFrontEndImplementation +import net.mamoe.mirai.console.MiraiConsoleFrontEnd +import net.mamoe.mirai.console.MiraiConsoleImplementation +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.plugin.DeferredPluginLoader +import net.mamoe.mirai.console.plugin.PluginLoader +import net.mamoe.mirai.console.plugin.jvm.JarPluginLoader +import net.mamoe.mirai.console.setting.MultiFileSettingStorage +import net.mamoe.mirai.console.setting.SettingStorage +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.utils.MiraiLogger +import java.io.File +import java.util.* + +/** + * mirai-console-pure 后端实现 + * + * @see MiraiConsoleFrontEndPure 前端实现 + * @see MiraiConsolePureLoader CLI 入口点 + */ +class MiraiConsoleImplementationPure +@JvmOverloads constructor( + override val rootDir: File = File("."), + override val builtInPluginLoaders: List> = Collections.unmodifiableList( + listOf( + DeferredPluginLoader { JarPluginLoader }) + ), + override val frontEnd: MiraiConsoleFrontEnd = MiraiConsoleFrontEndPure, + override val mainLogger: MiraiLogger = frontEnd.loggerFor("main"), + override val consoleCommandSender: ConsoleCommandSender = ConsoleCommandSenderImpl, + override val settingStorageForJarPluginLoader: SettingStorage = MultiFileSettingStorage(rootDir), + override val settingStorageForBuiltIns: SettingStorage = MultiFileSettingStorage(rootDir) +) : MiraiConsoleImplementation, CoroutineScope by CoroutineScope(SupervisorJob()) { + init { + rootDir.mkdir() + require(rootDir.isDirectory) { "rootDir ${rootDir.absolutePath} is not a directory" } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..771df81d6 --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt @@ -0,0 +1,129 @@ +/* + * 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( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "CANNOT_OVERRIDE_INVISIBLE_MEMBER", + "INVISIBLE_SETTER", + "INVISIBLE_GETTER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER", + "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPE_WARNING" +) +@file:OptIn(ConsoleInternalAPI::class) + +package net.mamoe.mirai.console.pure + +import kotlinx.coroutines.isActive +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start +import net.mamoe.mirai.console.command.* +import net.mamoe.mirai.console.utils.ConsoleInternalAPI +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.content +import net.mamoe.mirai.utils.DefaultLogger +import java.io.PrintStream +import kotlin.concurrent.thread + +/** + * mirai-console-pure CLI 入口点 + */ +object MiraiConsolePureLoader { + @JvmStatic + fun main(args: Array?) { + startup() + } +} + + +internal fun startup() { + DefaultLogger = { MiraiConsoleFrontEndPure.loggerFor(it) } + overrideSTD() + MiraiConsoleImplementationPure().start() + startConsoleThread() +} + +internal fun overrideSTD() { + System.setOut( + PrintStream( + BufferedOutputStream( + logger = DefaultLogger("sout").run { ({ line: String? -> info(line) }) } + ) + ) + ) + System.setErr( + PrintStream( + BufferedOutputStream( + logger = DefaultLogger("serr").run { ({ line: String? -> warning(line) }) } + ) + ) + ) +} + +internal fun startConsoleThread() { + thread(name = "Console Input") { + val consoleLogger = DefaultLogger("Console") + try { + kotlinx.coroutines.runBlocking { + while (isActive) { + val next = MiraiConsoleFrontEndPure.requestInput("").let { + when { + it.startsWith(CommandPrefix) -> { + it + } + it == "?" -> CommandPrefix + BuiltInCommands.Help.primaryName + else -> CommandPrefix + it + } + } + if (next.isBlank()) { + continue + } + consoleLogger.debug("INPUT> $next") + val result = ConsoleCommandSenderImpl.executeCommandDetailed(next) + when (result.status) { + CommandExecuteStatus.SUCCESSFUL -> { + } + CommandExecuteStatus.EXECUTION_EXCEPTION -> { + result.exception?.printStackTrace() + } + CommandExecuteStatus.COMMAND_NOT_FOUND -> { + consoleLogger.warning("Unknown command: ${result.commandName}") + } + CommandExecuteStatus.PERMISSION_DENIED -> { + consoleLogger.warning("Permission denied.") + } + + } + } + } + } catch (e: InterruptedException) { + return@thread + } + }.let { thread -> + MiraiConsole.job.invokeOnCompletion { + runCatching { + thread.interrupt() + }.exceptionOrNull()?.printStackTrace() + runCatching { + ConsoleUtils.terminal.close() + }.exceptionOrNull()?.printStackTrace() + } + } +} + +internal object ConsoleCommandSenderImpl : ConsoleCommandSender() { + override suspend fun sendMessage(message: Message) { + kotlin.runCatching { + ConsoleUtils.lineReader.printAbove(message.contentToString()) + }.onFailure { + println(message.content) + it.printStackTrace() + } + } +} \ No newline at end of file diff --git a/mirai-console-terminal/README.md b/frontend/mirai-console-terminal/README.md similarity index 100% rename from mirai-console-terminal/README.md rename to frontend/mirai-console-terminal/README.md diff --git a/mirai-console-terminal/build.gradle.kts b/frontend/mirai-console-terminal/build.gradle.kts similarity index 89% rename from mirai-console-terminal/build.gradle.kts rename to frontend/mirai-console-terminal/build.gradle.kts index 101775f61..cea7f593a 100644 --- a/mirai-console-terminal/build.gradle.kts +++ b/frontend/mirai-console-terminal/build.gradle.kts @@ -20,7 +20,7 @@ kotlin { all { languageSettings.useExperimentalAnnotation("kotlin.Experimental") - languageSettings.useExperimentalAnnotation("kotlin.OptIn") + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") languageSettings.progressiveMode = true languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") } @@ -28,7 +28,7 @@ kotlin { } dependencies { - compileOnly("net.mamoe:mirai-core-qqandroid:${Versions.Mirai.core}") + compileOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}") api(project(":mirai-console")) api(group = "com.googlecode.lanterna", name = "lanterna", version = "3.0.2") } diff --git a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt similarity index 98% rename from mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt index 9db3cd724..231efbf7f 100644 --- a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt +++ b/frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalFrontEnd.kt @@ -19,12 +19,12 @@ 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.MiraiConsoleTerminalUI.LoggerDrawer.cleanPage -import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.drawLog -import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.redrawLogs +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.MiraiConsoleUI +import net.mamoe.mirai.console.utils.MiraiConsoleFrontEnd import net.mamoe.mirai.utils.LoginSolver import net.mamoe.mirai.utils.SimpleLogger.LogPriority import java.awt.Image @@ -72,7 +72,7 @@ val String.isChineseChar: Boolean } -object MiraiConsoleTerminalUI : MiraiConsoleUI { +object MiraiConsoleTerminalFrontEnd : MiraiConsoleFrontEnd { const val cacheLogSize = 50 var mainTitle = "Mirai Console v0.01 Core v0.15" diff --git a/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 similarity index 84% rename from mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt rename to frontend/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt index 9da5cf122..cc4464ef5 100644 --- a/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 @@ -15,16 +15,16 @@ class MiraiConsoleTerminalLoader { println("[MiraiConsoleTerminalLoader]: 将以Pure[兼容模式]启动Console") MiraiConsole.start(MiraiConsoleUIPure()) } else { - MiraiConsoleTerminalUI.start() + MiraiConsoleTerminalFrontEnd.start() thread { MiraiConsole.start( - MiraiConsoleTerminalUI + MiraiConsoleTerminalFrontEnd ) } } Runtime.getRuntime().addShutdownHook(thread(start = false) { MiraiConsole.stop() - MiraiConsoleTerminalUI.exit() + MiraiConsoleTerminalFrontEnd.exit() }) } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 77f95ee0c..1ee034fe0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Wed Mar 04 22:27:09 CST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt deleted file mode 100644 index 660948f35..000000000 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.mamoe.mirai.console.graphical.model - -import javafx.beans.property.SimpleObjectProperty -import net.mamoe.mirai.Bot -import tornadofx.ItemViewModel -import tornadofx.getValue -import tornadofx.observableListOf -import tornadofx.setValue - -class BotModel(val uin: Long) { - val botProperty = SimpleObjectProperty(null) - var bot: Bot by botProperty - - val logHistory = observableListOf>() - val admins = observableListOf() -} - -class BotViewModel(botModel: BotModel? = null) : ItemViewModel(botModel) { - val bot = bind(BotModel::botProperty) - val logHistory = bind(BotModel::logHistory) - val admins = bind(BotModel::admins) -} \ No newline at end of file diff --git a/mirai-console/build.gradle.kts b/mirai-console/build.gradle.kts deleted file mode 100644 index 7ad3bc229..000000000 --- a/mirai-console/build.gradle.kts +++ /dev/null @@ -1,130 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import upload.Bintray -import java.util.* - -plugins { - kotlin("jvm") - kotlin("plugin.serialization") - id("java") - `maven-publish` - id("com.jfrog.bintray") -} - -apply(plugin = "com.github.johnrengelman.shadow") - -kotlin { - sourceSets { - all { - languageSettings.enableLanguageFeature("InlineClasses") - - languageSettings.useExperimentalAnnotation("kotlin.Experimental") - languageSettings.useExperimentalAnnotation("kotlin.OptIn") - languageSettings.progressiveMode = true - languageSettings.useExperimentalAnnotation("net.mamoe.mirai.utils.MiraiInternalAPI") - languageSettings.useExperimentalAnnotation("kotlin.ExperimentalUnsignedTypes") - languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference") - languageSettings.useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") - } - } -} -dependencies { - compileOnly("net.mamoe:mirai-core:${Versions.Mirai.core}") - compileOnly(kotlin("stdlib")) // embedded by core - - api("com.google.code.gson:gson:2.8.6") - api(group = "org.yaml", name = "snakeyaml", version = "1.25") - api(group = "com.moandjiezana.toml", name = "toml4j", version = "0.7.2") - api("org.jsoup:jsoup:1.12.1") - - api("org.jetbrains:annotations:19.0.0") - - testApi("net.mamoe:mirai-core-qqandroid:${Versions.Mirai.core}") - testApi(kotlin("stdlib")) -} - -version = Versions.Mirai.console - -description = "Console backend for mirai" - -val compileKotlin: KotlinCompile by tasks -compileKotlin.kotlinOptions { - jvmTarget = "1.8" -} -val compileTestKotlin: 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" -} - - - -tasks.register("ensureBintrayAvailable") { - doLast { - if (!Bintray.isBintrayAvailable(project)) { - error("bintray isn't available. ") - } - } -} - -if (Bintray.isBintrayAvailable(project)) { - bintray { - val keyProps = Properties() - val keyFile = file("../keys.properties") - if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) } - if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) } - - user = Bintray.getUser(project) - key = Bintray.getKey(project) - setPublications("mavenJava") - setConfigurations("archives") - - pkg.apply { - repo = "mirai" - name = "mirai-console" - setLicenses("AGPLv3") - publicDownloadNumbers = true - vcsUrl = "https://github.com/mamoe/mirai-console" - } - } - - @Suppress("DEPRECATION") - val sourcesJar by tasks.registering(Jar::class) { - classifier = "sources" - from(sourceSets.main.get().allSource) - } - - publishing { - /* - repositories { - maven { - // change to point to your repo, e.g. http://my.org/repo - url = uri("$buildDir/repo") - } - }*/ - publications { - register("mavenJava", MavenPublication::class) { - from(components["java"]) - - groupId = rootProject.group.toString() - artifactId = "mirai-console" - version = version - - pom.withXml { - val root = asNode() - root.appendNode("description", description) - root.appendNode("name", project.name) - root.appendNode("url", "https://github.com/mamoe/mirai") - root.children().last() - } - - artifact(sourcesJar.get()) - } - } - } -} else println("bintray isn't available. NO PUBLICATIONS WILL BE SET") \ No newline at end of file diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java b/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java deleted file mode 100644 index 5770709ae..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/command/JCommandManager.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.mamoe.mirai.console.command; - -// import jdk.jfr.Description; - -public class JCommandManager { - - private JCommandManager() { - - } - - public static CommandManager getInstance() { - return CommandManager.INSTANCE; - } - - -} \ No newline at end of file diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/events/EventListener.java b/mirai-console/src/main/java/net/mamoe/mirai/console/events/EventListener.java deleted file mode 100644 index bb7409ee6..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/events/EventListener.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.mamoe.mirai.console.events; - -import kotlinx.coroutines.GlobalScope; -import net.mamoe.mirai.console.plugins.PluginBase; -import net.mamoe.mirai.event.Event; -import net.mamoe.mirai.event.Listener; -import net.mamoe.mirai.event.ListeningStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Consumer; -import java.util.function.Function; - -public class EventListener { - - PluginBase base; - - public EventListener( - PluginBase base - ){ - this.base = base; - } - - /** - * 监听一个事件, 当 {@code onEvent} 返回 {@link ListeningStatus#STOPPED} 时停止监听. - * 机器人离线后不会停止监听. - * - * @param eventClass 事件类 - * @param onEvent 事件处理. 返回 {@link ListeningStatus#LISTENING} 时继续监听. - * @param 事件类型 - * @return 事件监听器. 可调用 {@link Listener#complete()} 或 {@link Listener#completeExceptionally(Throwable)} 让监听正常停止或异常停止. - */ - @NotNull - public Listener subscribe(@NotNull Class eventClass, @NotNull Function onEvent) { - return EventsImplKt.subscribeEventForJaptOnly(eventClass, base, onEvent); - } - - - /** - * 监听一个事件, 直到手动停止. - * 机器人离线后不会停止监听. - * - * @param eventClass 事件类 - * @param onEvent 事件处理. 返回 {@link ListeningStatus#LISTENING} 时继续监听. - * @param 事件类型 - * @return 事件监听器. 可调用 {@link Listener#complete()} 或 {@link Listener#completeExceptionally(Throwable)} 让监听正常停止或异常停止. - */ - @NotNull - public Listener subscribeAlways(@NotNull Class eventClass, @NotNull Consumer onEvent) { - return EventsImplKt.subscribeEventForJaptOnly(eventClass, base, onEvent); - } - -} diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/events/Events.java b/mirai-console/src/main/java/net/mamoe/mirai/console/events/Events.java deleted file mode 100644 index be017d241..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/events/Events.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.events; - -import kotlinx.coroutines.GlobalScope; -import net.mamoe.mirai.event.Event; -import net.mamoe.mirai.event.Listener; -import net.mamoe.mirai.event.ListeningStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * 事件处理 - */ -public final class Events { - /** - * 阻塞地广播一个事件. - * - * @param event 事件 - * @param 事件类型 - * @return {@code event} 本身 - */ - @NotNull - public static E broadcast(@NotNull E event) { - return EventsImplKt.broadcast(event); - } -} \ No newline at end of file diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/plugins/ConfigSectionFactory.java b/mirai-console/src/main/java/net/mamoe/mirai/console/plugins/ConfigSectionFactory.java deleted file mode 100644 index 0f1877e06..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/plugins/ConfigSectionFactory.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.mamoe.mirai.console.plugins; - -public class ConfigSectionFactory { - - public static ConfigSection create(){ - return ConfigSection.Companion.create(); - } - -} diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/scheduler/SchedulerTaskManager.java b/mirai-console/src/main/java/net/mamoe/mirai/console/scheduler/SchedulerTaskManager.java deleted file mode 100644 index 5c49b524a..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/scheduler/SchedulerTaskManager.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.mamoe.mirai.console.scheduler; - -/** - * Java开发者的SchedulerTask - * 使用kt实现, java的API - */ - -/** - * PluginScheduler.RepeatTaskReceipt repeatTaskReceipt = this.getScheduler().repeat(() -> { - * getLogger().info("I repeat"); - * },100); - * - * - * this.getScheduler().delay(() -> { - * repeatTaskReceipt.setCancelled(true); - * },10000); - * - * - * Future future = this.getScheduler().async(() -> { - * //do some task - * return "success"; - * }); - * - * try { - * getLogger().info(future.get()); - * } catch (InterruptedException | ExecutionException e) { - * e.printStackTrace(); - * } - */ - -public class SchedulerTaskManager { - public static SchedulerTaskManagerInstance getInstance(){ - return SchedulerTaskManagerInstance.INSTANCE; - } -} - diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java b/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java deleted file mode 100644 index 4b959c40a..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/BotManager.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.mamoe.mirai.console.utils; - -import net.mamoe.mirai.Bot; -import net.mamoe.mirai.console.MiraiConsole; - -import java.util.List; - -/** - * 获取Bot Manager - * Java友好API - */ -public class BotManager { - - public static List getManagers(long botAccount) { - Bot bot = MiraiConsole.INSTANCE.getBotOrThrow(botAccount); - return getManagers(bot); - } - - public static List getManagers(Bot bot){ - return BotHelperKt.getBotManagers(bot); - } - - public static boolean isManager(Bot bot, long target){ - return getManagers(bot).contains(target); - } - - public static boolean isManager(long botAccount, long target){ - return getManagers(botAccount).contains(target); - } -} - diff --git a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/Utils.java b/mirai-console/src/main/java/net/mamoe/mirai/console/utils/Utils.java deleted file mode 100644 index 6f2c654db..000000000 --- a/mirai-console/src/main/java/net/mamoe/mirai/console/utils/Utils.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.mamoe.mirai.console.utils; - -import org.jetbrains.annotations.Range; - -import java.util.concurrent.Callable; - -public final class Utils { - - /** - * 执行N次 callable - * 成功一次就会结束 - * 否则就会throw - */ - public static T tryNTimes(@Range(from = 1, to = Integer.MAX_VALUE) int n, - Callable callable) throws Exception { - Exception last = null; - - while (n-- > 0) { - try { - return callable.call(); - } catch (Exception e) { - if (last == null) { - last = e; - } else { - try { - last.addSuppressed(e); - } catch (Throwable ignored) { - } - } - } - } - - throw last; - } -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/CuiPluginCenter.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/CuiPluginCenter.kt deleted file mode 100644 index 394f9c2e5..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/CuiPluginCenter.kt +++ /dev/null @@ -1,130 +0,0 @@ -package net.mamoe.mirai.console.center - -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.request.get -import io.ktor.util.KtorExperimentalAPI -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.mamoe.mirai.console.plugins.PluginManager -import net.mamoe.mirai.console.utils.retryCatching -import net.mamoe.mirai.console.utils.tryNTimes -import java.io.File -import java.io.FileOutputStream -import java.net.HttpURLConnection -import java.net.URL - -internal object CuiPluginCenter : PluginCenter { - - var plugins: JsonArray? = 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 { - if (plugins == null) { - refresh() - } - if (it >= plugins!!.size()) { - return@forEach - } - val info = plugins!![it] - with(info.asJsonObject) { - map[this.get("name").asString] = PluginCenter.PluginInsight( - this.get("name")?.asString ?: "", - this.get("version")?.asString ?: "", - this.get("core")?.asString ?: "", - this.get("console")?.asString ?: "", - this.get("author")?.asString ?: "", - this.get("description")?.asString ?: "", - this.get("tags")?.asJsonArray?.map { it.asString } ?: arrayListOf(), - this.get("commands")?.asJsonArray?.map { it.asString } ?: arrayListOf() - ) - } - } - return map - } - - @OptIn(KtorExperimentalAPI::class) - private val Http = HttpClient(CIO) - - override suspend fun findPlugin(name: String): PluginCenter.PluginInfo? { - val result = retryCatching(3) { - Http.get("https://miraiapi.jasonczc.cn/getPluginDetailedInfo?name=$name") - }.recover { - return null - }.getOrNull() ?: return null - - if (result == "err:not found") { - return null - } - - return result.asJson().run { - PluginCenter.PluginInfo( - this.get("name")?.asString ?: "", - this.get("version")?.asString ?: "", - this.get("core")?.asString ?: "", - this.get("console")?.asString ?: "", - this.get("tags")?.asJsonArray?.map { it.asString } ?: arrayListOf(), - this.get("author")?.asString ?: "", - this.get("contact")?.asString ?: "", - this.get("description")?.asString ?: "", - this.get("usage")?.asString ?: "", - this.get("vsc")?.asString ?: "", - this.get("commands")?.asJsonArray?.map { it.asString } ?: arrayListOf(), - this.get("changeLog")?.asJsonArray?.map { it.asString } ?: arrayListOf() - ) - } - - } - - override suspend fun refresh() { - val results = Http.get("https://miraiapi.jasonczc.cn/getPluginList").asJson() - - if (!(results.has("success") && results["success"].asBoolean)) { - error("Failed to fetch plugin list from Cui Cloud") - } - plugins = results.get("result").asJsonArray//先不解析 - } - - override suspend fun T.downloadPlugin(name: String, progressListener: T.(Float) -> Unit): File { - 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() = "崔云" - - - private fun String.asJson(): JsonObject { - return JsonParser.parseString(this).asJsonObject - } - -} - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt deleted file mode 100644 index a7e061098..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt +++ /dev/null @@ -1,63 +0,0 @@ -package net.mamoe.mirai.console.center - -import java.io.File - -interface PluginCenter { - - data class PluginInsight( - val name: String, - val version: String, - val coreVersion: String, - val consoleVersion: String, - val author: String, - val description: String, - val tags: List, - val commands: List - ) - - data class PluginInfo( - val name: String, - val version: String, - val coreVersion: String, - val consoleVersion: String, - val tags: List, - val author: String, - val contact: String, - val description: String, - val usage: String, - val vcs: String, - val commands: List, - val changeLog: List - ) - - /** - * 获取一些中心的插件基本信息, - * 能获取到多少由实际的PluginCenter决定 - * 返回 插件名->Insight - */ - suspend fun fetchPlugin(page: Int): Map - - /** - * 尝试获取到某个插件 by 全名, case sensitive - * null 则没有 - */ - suspend fun findPlugin(name:String):PluginInfo? - - - suspend fun T.downloadPlugin(name:String, progressListener:T.(Float) -> Unit): File - - suspend fun downloadPlugin(name:String, progressListener:PluginCenter.(Float) -> Unit): File = downloadPlugin(name,progressListener) - - /** - * 刷新 - */ - suspend fun refresh() - - val name:String -} - -internal fun handleReplacement( - -){ - -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt deleted file mode 100644 index 29c9db7de..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.command - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.mamoe.mirai.console.plugins.PluginBase - -/** - * 指令 - * - * @see register 注册这个指令 - * @see registerCommand 注册指令 DSL - */ -interface Command { - /** - * 指令主名称 - */ - val name: String - - /** - * 别名 - */ - val alias: List - - /** - * 描述, 将会显示在 "/help" 指令中 - */ - val description: String - - /** - * 用法说明 - */ - val usage: String - - suspend fun onCommand(sender: CommandSender, args: List): Boolean -} - -abstract class AbstractCommand( - override val name: String, - override val alias: List, - override val description: String, - override val usage: String -) : Command - -/** - * 注册这个指令 - */ -inline fun Command.register(commandOwner: CommandOwner) = CommandManager.register(commandOwner, this) - -internal inline fun registerConsoleCommands(builder: CommandBuilder.() -> Unit): Command { - return CommandBuilder().apply(builder).register(ConsoleCommandOwner) -} - -/** - * 构造并注册一个指令 - */ -inline fun PluginBase.registerCommand(builder: CommandBuilder.() -> Unit): Command { - return CommandBuilder().apply(builder).register(this.asCommandOwner()) -} - - -// for java -@Suppress("unused") -abstract class BlockingCommand( - override val name: String, - override val alias: List = listOf(), - override val description: String = "", - override val usage: String = "" -) : Command { - /** - * 最高优先级监听器. - * - * 指令调用将优先触发 [Command.onCommand], 若该函数返回 `false`, 则不会调用 [PluginBase.onCommand] - * */ - final override suspend fun onCommand(sender: CommandSender, args: List): Boolean { - return withContext(Dispatchers.IO) { - onCommandBlocking(sender, args) - } - } - - abstract fun onCommandBlocking(sender: CommandSender, args: List): Boolean -} - -/** - * @see registerCommand - */ -class CommandBuilder @PublishedApi internal constructor() { - var name: String? = null - var alias: List? = null - var description: String = "" - var usage: String = "use /help for help" - - internal var onCommand: (suspend CommandSender.(args: List) -> Boolean)? = null - - fun onCommand(commandProcess: suspend CommandSender.(args: List) -> Boolean) { - onCommand = commandProcess - } -} - - -// internal - - -internal class AnonymousCommand internal constructor( - override val name: String, - override val alias: List, - override val description: String, - override val usage: String = "", - val onCommand: suspend CommandSender.(args: List) -> Boolean -) : Command { - override suspend fun onCommand(sender: CommandSender, args: List): Boolean { - return onCommand.invoke(sender, args) - } -} - -@PublishedApi -internal fun CommandBuilder.register(commandOwner: CommandOwner): AnonymousCommand { - if (name == null || onCommand == null) { - error("CommandBuilder not complete") - } - if (alias == null) { - alias = listOf() - } - return AnonymousCommand( - name!!, - alias!!, - description, - usage, - onCommand!! - ).also { it.register(commandOwner) } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt deleted file mode 100644 index 28d45513d..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandArgParser.kt +++ /dev/null @@ -1,286 +0,0 @@ -@file:Suppress("NOTHING_TO_INLINE") - -package net.mamoe.mirai.console.command - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.utils.fuzzySearchMember -import net.mamoe.mirai.contact.Friend -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.message.data.At -import net.mamoe.mirai.message.data.SingleMessage -import net.mamoe.mirai.message.data.content -import kotlin.contracts.contract - -/** - * this output type of that arg - * input is always String - */ -abstract class CommandArgParser { - abstract fun parse(s: String, sender: CommandSender): T - open fun parse(s: SingleMessage, sender: CommandSender): T = parse(s.content, sender) -} - -@Suppress("unused") -@JvmSynthetic -inline fun CommandArgParser<*>.illegalArgument(message: String, cause: Throwable? = null): Nothing { - throw ParserException(message, cause) -} - -@JvmSynthetic -inline fun CommandArgParser<*>.checkArgument( - condition: Boolean, - crossinline message: () -> String = { "Check failed." } -) { - contract { - returns() implies condition - } - if (!condition) illegalArgument(message()) -} - -/** - * 创建匿名 [CommandArgParser] - */ -@Suppress("FunctionName") -@JvmSynthetic -inline fun CommandArgParser( - crossinline parser: CommandArgParser.(s: String, sender: CommandSender) -> T -): CommandArgParser = object : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): T = parser(s, sender) -} - - -/** - * 在解析参数时遇到的 _正常_ 错误. 如参数不符合规范. - */ -class ParserException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) - - -object IntArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Int = - s.toIntOrNull() ?: illegalArgument("无法解析 $s 为整数") -} - -object LongArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Long = - s.toLongOrNull() ?: illegalArgument("无法解析 $s 为长整数") -} - -object ShortArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Short = - s.toShortOrNull() ?: illegalArgument("无法解析 $s 为短整数") -} - -object ByteArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Byte = - s.toByteOrNull() ?: illegalArgument("无法解析 $s 为字节") -} - -object DoubleArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Double = - s.toDoubleOrNull() ?: illegalArgument("无法解析 $s 为小数") -} - -object FloatArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Float = - s.toFloatOrNull() ?: illegalArgument("无法解析 $s 为小数") -} - -object StringArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): String = s -} - -object BooleanArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Boolean = s.trim().let { str -> - str.equals("true", ignoreCase = true) - || str.equals("yes", ignoreCase = true) - || str.equals("enabled", ignoreCase = true) - } -} - -/** - * require a bot that already login in console - * input: Bot UIN - * output: Bot - * errors: String->Int convert, Bot Not Exist - */ -object ExistBotArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Bot { - val uin = try { - s.toLong() - } catch (e: Exception) { - error("无法识别QQ UIN$s") - } - return try { - Bot.getInstance(uin) - } catch (e: NoSuchElementException) { - error("无法找到Bot $uin") - } - } -} - -object ExistFriendArgParser : CommandArgParser() { - //Bot.friend - //friend - //~ = self - override fun parse(s: String, sender: CommandSender): Friend { - if (s == "~") { - if (sender !is BotAware) { - illegalArgument("无法解析~作为默认") - } - val targetID = when (sender) { - is GroupContactCommandSender -> sender.realSender.id - is ContactCommandSender -> sender.contact.id - else -> illegalArgument("无法解析~作为默认") - } - return try { - sender.bot.friends[targetID] - } catch (e: NoSuchElementException) { - illegalArgument("无法解析~作为默认") - } - } - if (sender is BotAware) { - return try { - sender.bot.friends[s.toLong()] - } catch (e: NoSuchElementException) { - error("无法找到" + s + "这个好友") - } catch (e: NumberFormatException) { - error("无法解析$s") - } - } else { - s.split(".").let { args -> - if (args.size != 2) { - illegalArgument("无法解析 $s, 格式应为 机器人账号.好友账号") - } - return try { - Bot.getInstance(args[0].toLong()).friends.getOrNull( - args[1].toLongOrNull() ?: illegalArgument("无法解析 $s 为好友") - ) ?: illegalArgument("无法找到好友 ${args[1]}") - } catch (e: NoSuchElementException) { - illegalArgument("无法找到机器人账号 ${args[0]}") - } - } - } - } - - override fun parse(s: SingleMessage, sender: CommandSender): Friend { - if (s is At) { - assert(sender is GroupContactCommandSender) - return (sender as BotAware).bot.friends.getOrNull(s.target) ?: illegalArgument("At的对象非Bot好友") - } else { - error("无法解析 $s 为好友") - } - } -} - -object ExistGroupArgParser : CommandArgParser() { - override fun parse(s: String, sender: CommandSender): Group { - //by default - if ((s == "" || s == "~") && sender is GroupContactCommandSender) { - return sender.contact as Group - } - //from bot to group - if (sender is BotAware) { - val code = try { - s.toLong() - } catch (e: NoSuchElementException) { - error("无法识别Group Code$s") - } - return try { - sender.bot.getGroup(code) - } catch (e: NoSuchElementException) { - error("无法找到Group " + code + " from Bot " + sender.bot.id) - } - } - //from console/other - return with(s.split(".")) { - if (this.size != 2) { - error("请使用BotQQ号.群号 来表示Bot的一个群") - } - try { - Bot.getInstance(this[0].toLong()).getGroup(this[1].toLong()) - } catch (e: NoSuchElementException) { - error("无法找到" + this[0] + "的" + this[1] + "群") - } catch (e: NumberFormatException) { - error("无法识别群号或机器人UIN") - } - } - } -} - -object ExistMemberArgParser : CommandArgParser() { - //后台: Bot.Group.Member[QQ/名片] - //私聊: Group.Member[QQ/名片] - //群内: Q号 - //群内: 名片 - override fun parse(s: String, sender: CommandSender): Member { - if (sender !is BotAware) { - with(s.split(".")) { - checkArgument(this.size >= 3) { - "无法识别Member, 请使用Bot.Group.Member[QQ/名片]的格式" - } - - val bot = try { - Bot.getInstance(this[0].toLong()) - } catch (e: NoSuchElementException) { - illegalArgument("无法找到Bot") - } catch (e: NumberFormatException) { - illegalArgument("无法识别Bot") - } - - val group = try { - bot.getGroup(this[1].toLong()) - } catch (e: NoSuchElementException) { - illegalArgument("无法找到Group") - } catch (e: NumberFormatException) { - illegalArgument("无法识别Group") - } - - val memberIndex = this.subList(2, this.size).joinToString(".") - return group.members.getOrNull(memberIndex.toLong()) - ?: group.fuzzySearchMember(memberIndex) - ?: error("无法找到成员$memberIndex") - } - } else { - val bot = sender.bot - if (sender is GroupContactCommandSender) { - val group = sender.contact as Group - return try { - group.members[s.toLong()] - } catch (ignored: Exception) { - group.fuzzySearchMember(s) ?: illegalArgument("无法找到成员$s") - } - } else { - with(s.split(".")) { - if (this.size < 2) { - illegalArgument("无法识别Member, 请使用Group.Member[QQ/名片]的格式") - } - val group = try { - bot.getGroup(this[0].toLong()) - } catch (e: NoSuchElementException) { - illegalArgument("无法找到Group") - } catch (e: NumberFormatException) { - illegalArgument("无法识别Group") - } - - val memberIndex = this.subList(1, this.size).joinToString(".") - return try { - group.members[memberIndex.toLong()] - } catch (ignored: Exception) { - group.fuzzySearchMember(memberIndex) ?: illegalArgument("无法找到成员$memberIndex") - } - } - } - } - } - - override fun parse(s: SingleMessage, sender: CommandSender): Member { - return if (s is At) { - checkArgument(sender is GroupContactCommandSender) - (sender.contact as Group).members[s.target] - } else { - illegalArgument("无法识别Member" + s.content) - } - } -} - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt deleted file mode 100644 index 28c5164d9..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt +++ /dev/null @@ -1,139 +0,0 @@ -@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "unused", "MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.command - -import kotlin.reflect.KClass - -/** - * 指令描述. 包含名称, 权限要求, 参数解析器环境, 参数列表. - */ -class CommandDescriptor( - /** - * 包含子命令的全名. 如 "`group kick`", 其中 `kick` 为 `group` 的子命令 - */ - val fullName: String, - /** - * 指令参数解析器环境. - */ - val context: CommandParserContext, - /** - * 指令参数列表, 有顺序. - */ - val params: List>, - /** - * 指令权限 - * - * @see CommandPermission.or 要求其中一个权限 - * @see CommandPermission.and 同时要求两个权限 - */ - val permission: CommandPermission = CommandPermission.Default -) - -/** - * 构建一个 [CommandDescriptor] - */ -@Suppress("FunctionName") -inline fun CommandDescriptor( - fullName: String, - block: CommandDescriptorBuilder.() -> Unit -): CommandDescriptor = CommandDescriptorBuilder(fullName).apply(block).build() - -class CommandDescriptorBuilder( - val fullName: String -) { - @PublishedApi - internal var context: CommandParserContext = CommandParserContext.Builtins - - @PublishedApi - internal var permission: CommandPermission = CommandPermission.Default - - @PublishedApi - internal var params: MutableList> = mutableListOf() - - /** 增加指令参数解析器列表 */ - @JvmSynthetic - inline fun context(block: CommandParserContextBuilder.() -> Unit) { - this.context += CommandParserContext(block) - } - - /** 增加指令参数解析器列表 */ - @JvmSynthetic - inline fun context(context: CommandParserContext): CommandDescriptorBuilder = apply { - this.context += context - } - - /** 设置权限要求 */ - fun permission(permission: CommandPermission): CommandDescriptorBuilder = apply { - this.permission = permission - } - - /** 设置权限要求 */ - @JvmSynthetic - inline fun permission(crossinline block: CommandSender.() -> Boolean) { - this.permission = AnonymousCommandPermission(block) - } - - fun param(vararg params: CommandParam<*>): CommandDescriptorBuilder = apply { - this.params.addAll(params) - } - - @JvmSynthetic - fun param( - name: String?, - type: KClass, - overrideParser: CommandArgParser? = null - ): CommandDescriptorBuilder = apply { - this.params.add(CommandParam(name, type).apply { this.parser = overrideParser }) - } - - fun param( - name: String?, - type: Class, - overrideParser: CommandArgParser? = null - ): CommandDescriptorBuilder = - param(name, type, overrideParser) - - inline fun param( - name: String? = null, - overrideParser: CommandArgParser? = null - ): CommandDescriptorBuilder = - param(name, T::class, overrideParser) - - @JvmSynthetic - fun param(vararg pairs: Pair>): CommandDescriptorBuilder = apply { - for (pair in pairs) { - this.params.add(CommandParam(pair.first, pair.second)) - } - } - - @JvmSynthetic - fun params(block: ParamBlock.() -> Unit): CommandDescriptorBuilder = apply { - ParamBlock(params).apply(block) - } - - @JvmSynthetic - fun param(type: KClass<*>): CommandDescriptorBuilder = apply { - this.params.add(CommandParam(null, type)) - } - - fun param(type: Class<*>): CommandDescriptorBuilder = apply { - this.params.add(CommandParam(null, type.kotlin)) - } - - fun build(): CommandDescriptor = CommandDescriptor(fullName, context, params, permission) -} - -@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") -inline class ParamBlock internal constructor(@PublishedApi internal val list: MutableList>) { - /** 添加一个名称为 [this], 类型为 [klass] 的参数. 返回添加成功的对象 */ - infix fun String.typed(klass: KClass): CommandParam = - CommandParam(this, klass).also { list.add(it) } - - /** 指定 [CommandParam.overrideParser] */ - infix fun CommandParam.using(parser: CommandArgParser): CommandParam = - this.apply { this.parser = parser } - - /** 覆盖 [CommandArgParser] */ - inline infix fun String.using(parser: CommandArgParser): CommandParam = - this typed T::class using parser -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt deleted file mode 100644 index 7b9631341..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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") - -package net.mamoe.mirai.console.command - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.CommandManager.processCommandQueue -import net.mamoe.mirai.console.plugins.PluginBase -import net.mamoe.mirai.console.plugins.PluginManager -import java.util.concurrent.Executors - -interface CommandOwner - -class PluginCommandOwner(val pluginBase: PluginBase) : CommandOwner -internal object ConsoleCommandOwner : CommandOwner - -fun PluginBase.asCommandOwner() = PluginCommandOwner(this) - - -object CommandManager : Job by { - GlobalScope.launch(start = CoroutineStart.LAZY) { - processCommandQueue() - } -}() { - private val registeredCommand: MutableMap = mutableMapOf() - val commands: Collection get() = registeredCommand.values - private val pluginCommands: MutableMap> = mutableMapOf() - - internal fun clearPluginsCommands() { - pluginCommands.values.forEach { a -> - a.forEach { - unregister(it) - } - } - pluginCommands.clear() - } - - internal fun clearPluginCommands( - pluginBase: PluginBase - ) { - pluginCommands[pluginBase]?.run { - this.forEach { unregister(it) } - this.clear() - } - } - - /** - * 注册这个指令. - * - * @throws IllegalStateException 当已注册的指令与 [command] 重名时 - */ - fun register(commandOwner: CommandOwner, command: Command) { - val allNames = mutableListOf(command.name).also { it.addAll(command.alias) } - allNames.forEach { - if (registeredCommand.containsKey(it)) { - error("Command Name(or Alias) $it is already registered, consider if same functional plugin was installed") - } - } - allNames.forEach { - registeredCommand[it] = command - } - if (commandOwner is PluginCommandOwner) { - pluginCommands.computeIfAbsent(commandOwner.pluginBase) { mutableSetOf() }.add(command) - } - } - - fun register(pluginBase: PluginBase, command: Command) = - CommandManager.register(pluginBase.asCommandOwner(), command) - - fun unregister(command: Command) { - command.alias.forEach { - registeredCommand.remove(it) - } - registeredCommand.remove(command.name) - } - - fun unregister(commandName: String): Boolean { - return registeredCommand.remove(commandName) != null - } - - - /** - * 最基础的执行指令的方式 - * 指令将会被加入队列,依次执行 - * - * @param sender 指令执行者, 可为 [ConsoleCommandSender] 或 [ContactCommandSender] - */ - fun runCommand(sender: CommandSender, command: String) { - commandChannel.offer( - FullCommand(sender, command) - ) - } - - /** - * 插队异步执行一个指令并返回 [Deferred] - * - * @param sender 指令执行者, 可为 [ConsoleCommandSender] 或 [ContactCommandSender] - * @see PluginBase.runCommandAsync 扩展 - */ - fun runCommandAsync(pluginBase: PluginBase, sender: CommandSender, command: String): Deferred { - return pluginBase.async { - processCommand(sender, command) - } - } - - /** - * 插队执行一个指令并返回 [Deferred] - * - * @param sender 指令执行者, 可为 [ConsoleCommandSender] 或 [ContactCommandSender] - * @see PluginBase.runCommandAsync 扩展 - */ - @Suppress("KDocUnresolvedReference") - suspend fun dispatchCommand(sender: CommandSender, command: String): Boolean { - return processCommand(sender, command) - } - - - /** - * 阻塞当前线程, 插队执行一个指令 - * - * @param sender 指令执行者, 可为 [ConsoleCommandSender] 或 [ContactCommandSender] - */ - // for java - fun dispatchCommandBlocking(sender: CommandSender, command: String): Boolean = - runBlocking { dispatchCommand(sender, command) } - - - // internal - - /** - * 单线程执行指令 - */ - private val commandDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() - - private suspend fun processCommand(sender: CommandSender, fullCommand: String): Boolean { - return withContext(commandDispatcher) { - processCommandImpl(sender, fullCommand) - } - } - - private suspend fun processCommandImpl(sender: CommandSender, fullCommand: String): Boolean { - val blocks = fullCommand.split(" ") - val commandHead = blocks[0].removePrefix(DefaultCommands.commandPrefix) - val args = blocks.drop(1) - return registeredCommand[commandHead]?.run { - try { - return onCommand(sender, ArrayList(args)).also { - if (it) { - PluginManager.onCommand(this, sender, args) - } else { - sender.sendMessage(this.usage) - } - } - } catch (e: Exception) { - sender.sendMessage("在运行指令时出现了未知错误") - MiraiConsole.logger(e) - false - } finally { - (sender as AbstractCommandSender).flushMessage() - } - } ?: throw UnknownCommandException(commandHead) - } - - internal class FullCommand( - val sender: CommandSender, - val commandLine: String - ) - - private val commandChannel: Channel = Channel(Channel.UNLIMITED) - - private tailrec suspend fun processCommandQueue() { - val command = commandChannel.receive() - try { - processCommand(command.sender, command.commandLine) - } catch (e: UnknownCommandException) { - command.sender.sendMessage("未知指令 " + command.commandLine) - (command.sender as? ConsoleCommandSender)?.apply { - val cmd = command.commandLine.let { - val index = it.indexOf(' ') - if (index == -1) return@let it - return@let it.substring(0, index) - } - if (cmd.isNotEmpty()) { - if (cmd[0] == '/') { - registeredCommand[cmd.substring(1)]?.let { - sendMessage("请问你是不是想执行 `${command.commandLine.substring(1)}`") - } - } - } - } - } catch (e: Throwable) {//should never happen - MiraiConsole.logger(e) - } - if (isActive) { - processCommandQueue() - } - } -} - -/** - * 插队异步执行一个指令并返回 [Deferred] - * - * @param sender 指令执行者, 可为 [ConsoleCommandSender] 或 [ContactCommandSender] - * @see PluginBase.runCommandAsync 扩展 - */ -fun PluginBase.runCommandAsync(sender: CommandSender, command: String): Deferred = - CommandManager.runCommandAsync(this, sender, command) - - -class UnknownCommandException(command: String) : Exception("unknown command \"$command\"") \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt deleted file mode 100644 index edd85f319..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt +++ /dev/null @@ -1,36 +0,0 @@ -@file:Suppress("unused") - -package net.mamoe.mirai.console.command - -import kotlin.reflect.KClass - -/** - * 指令形式参数. - */ -data class CommandParam( - /** - * 参数名, 为 `null` 时即为匿名参数. - * 参数名允许重复 (尽管并不建议这样做). - * 参数名仅提供给 [CommandArgParser] 以发送更好的错误信息. - */ - val name: String?, - /** - * 参数类型. 将从 [CommandDescriptor.context] 中寻找 [CommandArgParser] 解析. - */ - val type: KClass // exact type -) { - constructor(name: String?, type: KClass, parser: CommandArgParser) : this(name, type) { - this.parser = parser - } - - @JvmField - internal var parser: CommandArgParser? = null - - - /** - * 覆盖的 [CommandArgParser]. - * - * 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser] - */ - val overrideParser: CommandArgParser? get() = parser -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt deleted file mode 100644 index af45d41a6..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandPermission.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("unused", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.command - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.utils.isManager -import net.mamoe.mirai.contact.isAdministrator -import net.mamoe.mirai.contact.isOperator -import net.mamoe.mirai.contact.isOwner - -/** - * 指令权限 - * - * @see AnonymousCommandPermission - */ -abstract class CommandPermission { - /** - * 判断 [this] 是否拥有这个指令的权限 - */ - abstract fun CommandSender.hasPermission(): Boolean - - - /** - * 满足两个权限其中一个即可使用指令 - */ // no extension for Java - infix fun or(another: CommandPermission): CommandPermission = OrCommandPermission(this, another) - - /** - * 同时拥有两个权限才能使用指令 - */ // no extension for Java - infix fun and(another: CommandPermission): CommandPermission = AndCommandPermission(this, another) - - - /** - * 任何人都可以使用这个指令 - */ - object Any : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean = true - } - - /** - * 任何人都不能使用这个指令. 指令只能通过代码在 [CommandManager] 使用 - */ - object None : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean = false - } - - /** - * 管理员或群主可以使用这个指令 - */ - class Operator( - /** - * 指定只有来自某个 [Bot] 的管理员或群主才可以使用这个指令 - */ - vararg val fromBot: Long - ) : CommandPermission() { - constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) - - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isOperator() - } - - /** - * 来自任何 [Bot] 的任何一个管理员或群主都可以使用这个指令 - */ - companion object Any : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.realSender.isOperator() - } - } - } - - /** - * 群主可以使用这个指令 - */ - class GroupOwner( - /** - * 指定只有来自某个 [Bot] 的群主才可以使用这个指令 - */ - vararg val fromBot: Long - ) : CommandPermission() { - constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) - - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isOwner() - } - - /** - * 来自任何 [Bot] 的任何一个群主都可以使用这个指令 - */ - companion object Any : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.realSender.isOwner() - } - } - } - - /** - * 管理员 (不包含群主) 可以使用这个指令 - */ - class Administrator( - /** - * 指定只有来自某个 [Bot] 的管理员 (不包含群主) 才可以使用这个指令 - */ - vararg val fromBot: Long - ) : CommandPermission() { - constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) - - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isAdministrator() - } - - /** - * 来自任何 [Bot] 的任何一个管理员 (不包含群主) 都可以使用这个指令 - */ - companion object Any : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.realSender.isAdministrator() - } - } - } - - /** - * console 管理员可以使用这个指令 - */ - class Manager( - /** - * 指定只有来自某个 [Bot] 的管理员或群主才可以使用这个指令 - */ - vararg val fromBot: Long - ) : CommandPermission() { - constructor(vararg fromBot: Bot) : this(*fromBot.map { it.id }.toLongArray()) - - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.bot.id in fromBot && this.realSender.isManager - } - - /** - * 任何 [Bot] 的 manager 都可以使用这个指令 - */ - companion object Any : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this is GroupContactCommandSender && this.realSender.isManager - } - } - } - - /** - * 仅控制台能使用和这个指令 - */ - object Console : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean = false - } - - companion object { - @JvmStatic - val Default: CommandPermission = Manager or Console - } -} - -/** - * 使用 [lambda][block] 快速构造 [CommandPermission] - */ -@JvmSynthetic -@Suppress("FunctionName") -inline fun AnonymousCommandPermission(crossinline block: CommandSender.() -> Boolean): CommandPermission { - return object : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean = block() - } -} - -inline fun CommandSender.hasPermission(permission: CommandPermission): Boolean = - permission.run { this@hasPermission.hasPermission() } - - -inline fun CommandPermission.hasPermission(sender: CommandSender): Boolean = this.run { sender.hasPermission() } - - -internal class OrCommandPermission( - private val first: CommandPermission, - private val second: CommandPermission -) : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this.hasPermission(first) || this.hasPermission(second) - } -} - -internal class AndCommandPermission( - private val first: CommandPermission, - private val second: CommandPermission -) : CommandPermission() { - override fun CommandSender.hasPermission(): Boolean { - return this.hasPermission(first) || this.hasPermission(second) - } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt deleted file mode 100644 index 9654fc5f1..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandSender.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.command - -import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.message.data.Message - -/** - * 指令发送者 - * - * @see AbstractCommandSender 请继承于该抽象类 - */ -interface CommandSender { - /** - * 立刻发送一条消息 - */ - suspend fun sendMessage(messageChain: Message) - - suspend fun sendMessage(message: String) - /** - * 写入要发送的内容 所有内容最后会被以一条发出 - */ - fun appendMessage(message: String) - - fun sendMessageBlocking(messageChain: Message) = runBlocking { sendMessage(messageChain) } - fun sendMessageBlocking(message: String) = runBlocking { sendMessage(message) } -} - - -abstract class AbstractCommandSender : CommandSender { - internal val builder = StringBuilder() - - override fun appendMessage(message: String) { - builder.appendln(message) - } - - internal open suspend fun flushMessage() { - if (builder.isNotEmpty()) { - sendMessage(builder.toString().removeSuffix("\n")) - } - } -} - -/** - * 控制台指令执行者. 代表由控制台执行指令 - */ -object ConsoleCommandSender : AbstractCommandSender() { - override suspend fun sendMessage(messageChain: Message) { - MiraiConsole.logger("[Command]", 0, messageChain.toString()) - } - - override suspend fun sendMessage(message: String) { - MiraiConsole.logger("[Command]", 0, message) - } - - override suspend fun flushMessage() { - super.flushMessage() - builder.clear() - } -} - -/** - * 指向性CommandSender - * 你可以获得用户在和哪个Bot说指令 - */ -interface BotAware{ - val bot:Bot -} - - -/** - * 联系人指令执行者. 代表由一个 QQ 用户私聊执行指令 - */ -@Suppress("MemberVisibilityCanBePrivate") -open class ContactCommandSender(override val bot: Bot, val contact: Contact) : AbstractCommandSender(), BotAware{ - override suspend fun sendMessage(messageChain: Message) { - contact.sendMessage(messageChain) - } - - override suspend fun sendMessage(message: String) { - contact.sendMessage(message) - } -} - -/** - * 联系人指令执行者. 代表由一个 QQ 用户 在群里执行指令 - */ -open class GroupContactCommandSender( - bot: Bot, - val realSender: Member, - subject: Contact -):ContactCommandSender(bot,subject) \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/events/EventsImpl.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/events/EventsImpl.kt deleted file mode 100644 index eaf9e99d9..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/events/EventsImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - -package net.mamoe.mirai.console.events; - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.event.Event -import net.mamoe.mirai.event.Listener -import net.mamoe.mirai.event.ListeningStatus -import net.mamoe.mirai.event.broadcast -import net.mamoe.mirai.event.internal._subscribeEventForJaptOnly -import java.util.function.Consumer -import java.util.function.Function - -internal fun broadcast(e: E): E = runBlocking { e.broadcast() } - -internal fun Class.subscribeEventForJaptOnly( - scope: CoroutineScope, - onEvent: Function -): Listener = _subscribeEventForJaptOnly(scope, onEvent) - -internal fun Class.subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer): Listener = - _subscribeEventForJaptOnly(scope, onEvent) \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt deleted file mode 100644 index ba8225913..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt +++ /dev/null @@ -1,617 +0,0 @@ -/* - * 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("MemberVisibilityCanBePrivate") - -package net.mamoe.mirai.console.plugins - -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.moandjiezana.toml.Toml -import com.moandjiezana.toml.TomlWriter -import kotlinx.serialization.Serializable -import kotlinx.serialization.UnstableDefault -import net.mamoe.mirai.console.encodeToString -import net.mamoe.mirai.utils.MiraiInternalAPI -import org.yaml.snakeyaml.Yaml -import java.io.File -import java.io.InputStream -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import kotlin.NoSuchElementException -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KClass -import kotlin.reflect.KProperty -import kotlin.reflect.full.isSubclassOf - - -/** - * 标注一个即将在将来版本删除的 API - * - * 目前 [Config] 的读写设计语义不明, list 和 map 读取效率低, 属性委托写入语义不明 - * 将在未来重写并变为 `ReadOnlyConfig` 和 `ReadWriteConfig`. - * 并计划添加图形端配置绑定, 和带注释的序列化, 自动保存的配置文件, 与指令绑定的属性. - */ -@RequiresOptIn("将在未来进行不兼容的修改", RequiresOptIn.Level.WARNING) -annotation class ToBeRemoved - -/** - * 可读可写配置文件. - * - * @suppress 注意: 配置文件正在进行不兼容的修改 - */ -interface Config { - @ToBeRemoved - fun getConfigSection(key: String): ConfigSection - - fun getString(key: String): String - fun getInt(key: String): Int - fun getFloat(key: String): Float - fun getDouble(key: String): Double - fun getLong(key: String): Long - fun getBoolean(key: String): Boolean - - @ToBeRemoved - fun getList(key: String): List<*> - - - @ToBeRemoved - fun getStringList(key: String): List - - @ToBeRemoved - fun getIntList(key: String): List - - @ToBeRemoved - fun getFloatList(key: String): List - - @ToBeRemoved - fun getDoubleList(key: String): List - - @ToBeRemoved - fun getLongList(key: String): List - - @ToBeRemoved - fun getConfigSectionList(key: String): List - - operator fun set(key: String, value: Any) - operator fun get(key: String): Any? - operator fun contains(key: String): Boolean - - @ToBeRemoved - fun exist(key: String): Boolean - - /** - * 设置 key = value (如果value不存在则valueInitializer会被调用) - * 之后返回当前key对应的值 - * */ - @ToBeRemoved - fun setIfAbsent(key: String, value: T) - - @ToBeRemoved - fun setIfAbsent(key: String, valueInitializer: Config.() -> T) - - @ToBeRemoved - fun asMap(): Map - - @ToBeRemoved - fun save() - - companion object { - @ToBeRemoved - fun load(fileName: String): Config { - return load( - File( - fileName.replace( - "//", - "/" - ) - ) - ) - } - - /** - * create a read-write config - * */ - @ToBeRemoved - fun load(file: File): Config { - if (!file.exists()) { - file.createNewFile() - } - return when (file.extension.toLowerCase()) { - "json" -> JsonConfig(file) - "yml" -> YamlConfig(file) - "yaml" -> YamlConfig(file) - "mirai" -> YamlConfig(file) - "ini" -> TomlConfig(file) - "toml" -> TomlConfig(file) - "properties" -> TomlConfig(file) - "property" -> TomlConfig(file) - "data" -> TomlConfig(file) - else -> error("Unsupported file config type ${file.extension.toLowerCase()}") - } - } - - /** - * create a read-only config - */ - @ToBeRemoved - fun load(content: String, type: String): Config { - return when (type.toLowerCase()) { - "json" -> JsonConfig(content) - "yml" -> YamlConfig(content) - "yaml" -> YamlConfig(content) - "mirai" -> YamlConfig(content) - "ini" -> TomlConfig(content) - "toml" -> TomlConfig(content) - "properties" -> TomlConfig(content) - "property" -> TomlConfig(content) - "data" -> TomlConfig(content) - else -> error("Unsupported file config type $content") - } - } - - /** - * create a read-only config - */ - @ToBeRemoved - fun load(inputStream: InputStream, type: String): Config { - return load(inputStream.readBytes().encodeToString(), type) - } - - } -} - - -@ToBeRemoved -fun File.loadAsConfig(): Config { - return Config.load(this) -} - -/* 最简单的代理 */ -@ToBeRemoved -inline operator fun Config.getValue(thisRef: Any?, property: KProperty<*>): T { - return smartCast(property) -} - -@ToBeRemoved -inline operator fun Config.setValue(thisRef: Any?, property: KProperty<*>, value: T) { - this[property.name] = value -} - -/* 带有默认值的代理 */ -@Suppress("unused") -@ToBeRemoved -inline fun Config.withDefault( - crossinline defaultValue: () -> T -): ReadWriteProperty { - return object : ReadWriteProperty { - override fun getValue(thisRef: Any, property: KProperty<*>): T { - if (this@withDefault.exist(property.name)) {//unsafe - return this@withDefault.smartCast(property) - } - return defaultValue() - } - - override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { - this@withDefault[property.name] = value - } - } -} - -/* 带有默认值且如果为空会写入的代理 */ -@Suppress("unused") -@ToBeRemoved -inline fun Config.withDefaultWrite( - noinline defaultValue: () -> T -): WithDefaultWriteLoader { - return WithDefaultWriteLoader( - T::class, - this, - defaultValue, - false - ) -} - -/* 带有默认值且如果为空会写入保存的代理 */ -@ToBeRemoved -inline fun Config.withDefaultWriteSave( - noinline defaultValue: () -> T -): WithDefaultWriteLoader { - return WithDefaultWriteLoader(T::class, this, defaultValue, true) -} - -@ToBeRemoved -class WithDefaultWriteLoader( - private val _class: KClass, - private val config: Config, - private val defaultValue: () -> T, - private val save: Boolean -) { - operator fun provideDelegate( - thisRef: Any, - prop: KProperty<*> - ): ReadWriteProperty { - val defaultValue by lazy { defaultValue.invoke() } - if (!config.contains(prop.name)) { - config[prop.name] = defaultValue - if (save) { - config.save() - } - } - return object : ReadWriteProperty { - override fun getValue(thisRef: Any, property: KProperty<*>): T { - if (config.exist(property.name)) {//unsafe - return config.smartCastInternal(property.name, _class) - } - return defaultValue - } - - override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { - config[property.name] = value - } - } - } -} - -@PublishedApi -internal inline fun Config.smartCast(property: KProperty<*>): T { - return smartCastInternal(property.name, T::class) -} - -@OptIn(ToBeRemoved::class) -@PublishedApi -@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") -internal fun Config.smartCastInternal(propertyName: String, _class: KClass): T { - return when (_class) { - String::class -> this.getString(propertyName) - Int::class -> this.getInt(propertyName) - Float::class -> this.getFloat(propertyName) - Double::class -> this.getDouble(propertyName) - Long::class -> this.getLong(propertyName) - Boolean::class -> this.getBoolean(propertyName) - else -> when { - _class.isSubclassOf(ConfigSection::class) -> this.getConfigSection(propertyName) - _class == List::class || _class == MutableList::class -> { - val list = this.getList(propertyName) - return if (list.isEmpty()) { - list - } else { - when (list[0]!!::class) { - String::class -> getStringList(propertyName) - Int::class -> getIntList(propertyName) - Float::class -> getFloatList(propertyName) - Double::class -> getDoubleList(propertyName) - Long::class -> getLongList(propertyName) - //不去支持getConfigSectionList(propertyName) - // LinkedHashMap::class -> getConfigSectionList(propertyName)//faster approach - else -> { - //if(list[0]!! is ConfigSection || list[0]!! is Map<*,*>){ - // getConfigSectionList(propertyName) - //}else { - error("unsupported type" + list[0]!!::class) - //} - } - } - } as T - } - else -> { - error("unsupported type") - } - } - } as T -} - - -@ToBeRemoved -interface ConfigSection : Config, MutableMap { - companion object { - fun create(): ConfigSection { - return ConfigSectionImpl() - } - - fun new(): ConfigSection { - return this.create() - } - } - - override fun getConfigSection(key: String): ConfigSection { - val content = get(key) ?: throw NoSuchElementException(key) - if (content is ConfigSection) { - return content - } - @Suppress("UNCHECKED_CAST") - return ConfigSectionDelegation( - Collections.synchronizedMap( - (get(key) ?: throw NoSuchElementException(key)) as LinkedHashMap - ) - ) - } - - override fun getString(key: String): String { - return (get(key) ?: throw NoSuchElementException(key)).toString() - } - - override fun getInt(key: String): Int { - return (get(key) ?: throw NoSuchElementException(key)).toString().toInt() - } - - override fun getFloat(key: String): Float { - return (get(key) ?: throw NoSuchElementException(key)).toString().toFloat() - } - - override fun getBoolean(key: String): Boolean { - return (get(key) ?: throw NoSuchElementException(key)).toString().toBoolean() - } - - override fun getDouble(key: String): Double { - return (get(key) ?: throw NoSuchElementException(key)).toString().toDouble() - } - - override fun getLong(key: String): Long { - return (get(key) ?: throw NoSuchElementException(key)).toString().toLong() - } - - override fun getList(key: String): List<*> { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>) - } - - override fun getStringList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).filterNotNull().map { it.toString() } - } - - override fun getIntList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).map { it.toString().toInt() } - } - - override fun getFloatList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).map { it.toString().toFloat() } - } - - override fun getDoubleList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).map { it.toString().toDouble() } - } - - override fun getLongList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).map { it.toString().toLong() } - } - - override fun getConfigSectionList(key: String): List { - return ((get(key) ?: throw NoSuchElementException(key)) as List<*>).map { - if (it is ConfigSection) { - it - } else { - @Suppress("UNCHECKED_CAST") - ConfigSectionDelegation( - Collections.synchronizedMap( - it as MutableMap - ) - ) - } - } - } - - override fun exist(key: String): Boolean { - return get(key) != null - } - - override fun setIfAbsent(key: String, value: T) { - putIfAbsent(key, value) - } - - override fun setIfAbsent(key: String, valueInitializer: Config.() -> T) { - if (this.exist(key)) { - put(key, valueInitializer.invoke(this)) - } - } -} - -@OptIn(ToBeRemoved::class) -internal inline fun ConfigSection.smartGet(key: String): T { - return this.smartCastInternal(key, T::class) -} - -@Serializable -@ToBeRemoved -open class ConfigSectionImpl : ConcurrentHashMap(), - - ConfigSection { - override fun set(key: String, value: Any) { - super.put(key, value) - } - - override operator fun get(key: String): Any? { - return super.get(key) - } - - @Suppress("RedundantOverride") - override fun contains(key: String): Boolean { - return super.contains(key) - } - - override fun exist(key: String): Boolean { - return containsKey(key) - } - - override fun asMap(): Map { - return this - } - - override fun save() { - - } -} - -@ToBeRemoved -open class ConfigSectionDelegation( - private val delegate: MutableMap -) : ConfigSection, MutableMap by delegate { - override fun set(key: String, value: Any) { - delegate[key] = value - } - - override fun contains(key: String): Boolean { - return delegate.containsKey(key) - } - - override fun asMap(): Map { - return delegate - } - - override fun save() { - - } -} - - -@ToBeRemoved -interface FileConfig : Config { - fun deserialize(content: String): ConfigSection - - fun serialize(config: ConfigSection): String -} - - -@MiraiInternalAPI -@ToBeRemoved -abstract class FileConfigImpl internal constructor( - private val rawContent: String -) : FileConfig, - ConfigSection { - - internal var file: File? = null - - - @Suppress("unused") - constructor(file: File) : this(file.readText()) { - this.file = file - } - - - private val content by lazy { - deserialize(rawContent) - } - - - override val size: Int get() = content.size - override val entries: MutableSet> get() = content.entries - override val keys: MutableSet get() = content.keys - override val values: MutableCollection get() = content.values - override fun containsKey(key: String): Boolean = content.containsKey(key) - override fun containsValue(value: Any): Boolean = content.containsValue(value) - override fun put(key: String, value: Any): Any? = content.put(key, value) - override fun isEmpty(): Boolean = content.isEmpty() - override fun putAll(from: Map) = content.putAll(from) - override fun clear() = content.clear() - override fun remove(key: String): Any? = content.remove(key) - - override fun save() { - if (isReadOnly) { - error("Config is readonly") - } - if (!((file?.exists())!!)) { - file?.createNewFile() - } - file?.writeText(serialize(content)) - } - - val isReadOnly: Boolean get() = file == null - - override fun contains(key: String): Boolean { - return content.contains(key) - } - - override fun get(key: String): Any? { - return content[key] - } - - override fun set(key: String, value: Any) { - content[key] = value - } - - override fun asMap(): Map { - return content.asMap() - } - -} - -@ToBeRemoved -@OptIn(MiraiInternalAPI::class) -class JsonConfig internal constructor( - content: String -) : FileConfigImpl(content) { - constructor(file: File) : this(file.readText()) { - this.file = file - } - - @UnstableDefault - override fun deserialize(content: String): ConfigSection { - if (content.isEmpty() || content.isBlank() || content == "{}") { - return ConfigSectionImpl() - } - val gson = Gson() - val typeRef = object : TypeToken>() {}.type - return ConfigSectionDelegation( - gson.fromJson(content, typeRef) - ) - } - - @UnstableDefault - override fun serialize(config: ConfigSection): String { - val gson = Gson() - return gson.toJson(config.toMap()) - } -} - -@ToBeRemoved -@OptIn(MiraiInternalAPI::class) -class YamlConfig internal constructor(content: String) : FileConfigImpl(content) { - constructor(file: File) : this(file.readText()) { - this.file = file - } - - override fun deserialize(content: String): ConfigSection { - if (content.isEmpty() || content.isBlank()) { - return ConfigSectionImpl() - } - return ConfigSectionDelegation( - Collections.synchronizedMap( - Yaml().load(content) as LinkedHashMap - ) - ) - } - - override fun serialize(config: ConfigSection): String { - return Yaml().dumpAsMap(config) - } - -} - -@ToBeRemoved -@OptIn(MiraiInternalAPI::class) -class TomlConfig internal constructor(content: String) : FileConfigImpl(content) { - constructor(file: File) : this(file.readText()) { - this.file = file - } - - override fun deserialize(content: String): ConfigSection { - if (content.isEmpty() || content.isBlank()) { - return ConfigSectionImpl() - } - return ConfigSectionDelegation( - Collections.synchronizedMap( - Toml().read(content).toMap() - ) - ) - - } - - override fun serialize(config: ConfigSection): String { - return TomlWriter().write(config) - } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt deleted file mode 100644 index 6baf165c5..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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("MemberVisibilityCanBePrivate", "unused") - -package net.mamoe.mirai.console.plugins - -import kotlinx.coroutines.* -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.Command -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.console.events.EventListener -import net.mamoe.mirai.console.scheduler.PluginScheduler -import net.mamoe.mirai.utils.MiraiLogger -import net.mamoe.mirai.utils.SimpleLogger -import java.io.File -import java.io.InputStream -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -/** - * 所有插件的基类 - */ -abstract class PluginBase -@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope { - final override val coroutineContext: CoroutineContext = coroutineContext + SupervisorJob() - - /** - * 插件被分配的数据目录。数据目录会与插件名称同名。 - */ - val dataFolder: File by lazy { - File(PluginManager.pluginsPath + "/" + PluginManager.lastPluginName) - .also { it.mkdir() } - } - - /** - * 当一个插件被加载时调用 - */ - open fun onLoad() { - - } - - /** - * 当插件被启用时调用. - * 此时所有其他插件都已经被调用了 [onLoad] - */ - open fun onEnable() { - - } - - /** - * 当插件关闭前被调用 - */ - open fun onDisable() { - - } - - /** - * 当任意指令被使用时调用. - * - * 指令调用将优先触发 [Command.onCommand], 若该函数返回 `false`, 则不会调用 [PluginBase.onCommand] - */ - open fun onCommand(command: Command, sender: CommandSender, args: List) { - - } - - /** - * 加载一个 [dataFolder] 中的 [Config] - */ - fun loadConfig(fileName: String): Config { - @OptIn(ToBeRemoved::class) - return Config.load(dataFolder.absolutePath + "/" + fileName) - } - - /** - * 插件的日志 - */ - val logger: MiraiLogger by lazy { - SimpleLogger("Plugin $pluginName") { priority, message, e -> - val identityString = "[${pluginName}]" - MiraiConsole.logger(priority, identityString, 0, message) - if (e != null) { - MiraiConsole.logger(priority, identityString, 0, e) - } - } - } - - /** - * 加载 resources 中的文件 - */ - fun getResources(fileName: String): InputStream? { - return try { - this.javaClass.classLoader.getResourceAsStream(fileName) - } catch (e: Exception) { - PluginManager.getFileInJarByName( - this.pluginName, - fileName - ) - } - } - - /** - * 加载 resource 中的 [Config] - * 这个 [Config] 是只读的 - */ - @ToBeRemoved - fun getResourcesConfig(fileName: String): Config { - require(fileName.contains(".")) { "Unknown Config Type" } - @OptIn(ToBeRemoved::class) - return Config.load(getResources(fileName) ?: error("No such file: $fileName"), fileName.substringAfter('.')) - } - - /** - * Java API Scheduler - */ - val scheduler: PluginScheduler? = PluginScheduler(this.coroutineContext) - - /** - * Java API EventListener - */ - val eventListener: EventListener = EventListener(@Suppress("LeakingThis") this) - - - // internal - - private var loaded = false - private var enabled = false - - internal fun load() { - if (!loaded) { - onLoad() - loaded = true - } - } - - internal fun enable() { - if (!enabled) { - onEnable() - enabled = true - } - } - - internal fun disable(throwable: CancellationException? = null) { - if (enabled) { - this.coroutineContext[Job]!!.cancelChildren(throwable) - try { - onDisable() - } catch (e: Exception) { - logger.error(e) - } - enabled = false - } - } - - internal var pluginName: String = "" -} - -/** - * 插件描述 - * @see PluginBase.description - */ -class PluginDescription( - val file: File, - val name: String, - val author: String, - val basePath: String, - val version: String, - val info: String, - val depends: List,//插件的依赖 - internal var loaded: Boolean = false, - internal var noCircularDepend: Boolean = true -) { - override fun toString(): String { - return "name: $name\nauthor: $author\npath: $basePath\nver: $version\ninfo: $info\ndepends: $depends" - } - - companion object { - @OptIn(ToBeRemoved::class) - fun readFromContent(content_: String, file: File): PluginDescription { - with(Config.load(content_, "yml")) { - try { - return PluginDescription( - file = file, - name = this.getString("name"), - author = kotlin.runCatching { - this.getString("author") - }.getOrElse { - "unknown" - }, - basePath = kotlin.runCatching { - this.getString("path") - }.getOrElse { - this.getString("main") - }, - version = kotlin.runCatching { - this.getString("version") - }.getOrElse { - "unknown" - }, - info = kotlin.runCatching { - this.getString("info") - }.getOrElse { - "unknown" - }, - depends = kotlin.runCatching { - this.getStringList("depends") - }.getOrElse { - listOf() - } - ) - } catch (e: Exception) { - error("Failed to read Plugin.YML") - } - } - } - } -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt deleted file mode 100644 index 38b068946..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt +++ /dev/null @@ -1,403 +0,0 @@ -/* - * 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", "unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - -package net.mamoe.mirai.console.plugins - -import kotlinx.coroutines.CancellationException -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.command.Command -import net.mamoe.mirai.console.command.CommandManager -import net.mamoe.mirai.console.command.CommandSender -import net.mamoe.mirai.console.encodeToString -import net.mamoe.mirai.utils.LockFreeLinkedList -import net.mamoe.mirai.utils.SimpleLogger -import java.io.File -import java.io.InputStream -import java.net.JarURLConnection -import java.net.URL -import java.util.jar.JarFile - -val PluginBase.description: PluginDescription get() = PluginManager.getPluginDescription(this) - -object PluginManager { - /** - * 通过插件获取介绍 - * @see description - */ - fun getPluginDescription(base: PluginBase): PluginDescription { - nameToPluginBaseMap.forEach { (s, pluginBase) -> - if (pluginBase == base) { - return pluginDescriptions[s]!! - } - } - error("can not find plugin description") - } - - /** - * 获取所有插件摘要 - */ - fun getAllPluginDescriptions(): Collection { - return pluginDescriptions.values - } - - /** - * 关闭所有插件 - */ - @JvmOverloads - fun disablePlugins(throwable: CancellationException? = null) { - CommandManager.clearPluginsCommands() - pluginsSequence.forEach { - it.disable(throwable) - } - nameToPluginBaseMap.clear() - pluginDescriptions.clear() - pluginsLoader.clear() - pluginsSequence.clear() - } - - /** - * 重载所有插件 - */ - fun reloadPlugins() { - pluginsSequence.forEach { - it.disable() - } - loadPlugins(false) - } - - /** - * 尝试加载全部插件 - */ - fun loadPlugins(clear: Boolean = true) = loadPluginsImpl(clear) - - - ////////////////// - //// internal //// - ////////////////// - - internal val pluginsPath = (MiraiConsole.path + "/plugins/").replace("//", "/").also { - File(it).mkdirs() - } - - private val logger = SimpleLogger("Plugin Manager") { p, message, e -> - MiraiConsole.logger(p, "[Plugin Manager]", 0, message) - MiraiConsole.logger(p, "[Plugin Manager]", 0, e) - } - - /** - * 加载成功的插件, 名字->插件 - */ - internal val nameToPluginBaseMap: MutableMap = mutableMapOf() - - /** - * 加载成功的插件, 名字->插件摘要 - */ - private val pluginDescriptions: MutableMap = mutableMapOf() - - /** - * 加载插件的PluginsLoader - */ - private val pluginsLoader: PluginsLoader = PluginsLoader(this.javaClass.classLoader) - - /** - * 插件优先级队列 - * 任何操作应该按这个Sequence顺序进行 - * 他的优先级取决于依赖, - * 在这个队列中, 被依赖的插件会在依赖的插件之前 - */ - private val pluginsSequence: LockFreeLinkedList = LockFreeLinkedList() - - - /** - * 广播Command方法 - */ - internal fun onCommand(command: Command, sender: CommandSender, args: List) { - pluginsSequence.forEach { - try { - it.onCommand(command, sender, args) - } catch (e: Throwable) { - logger.info(e) - } - } - } - - - @Volatile - internal var lastPluginName: String = "" - - /** - * 判断文件名/插件名是否已加载 - */ - private fun isPluginLoaded(file: File, name: String): Boolean { - pluginDescriptions.forEach { - if (it.key == name || it.value.file == file) { - return true - } - } - return false - } - - /** - * 寻找所有安装的插件(在文件夹), 并将它读取, 记录位置 - * 这个不等同于加载的插件, 可以理解为还没有加载的插件 - */ - internal data class FindPluginsResult( - val pluginsLocation: MutableMap, - val pluginsFound: MutableMap - ) - - internal fun findPlugins(): FindPluginsResult { - val pluginsLocation: MutableMap = mutableMapOf() - val pluginsFound: MutableMap = mutableMapOf() - - File(pluginsPath).listFiles()?.forEach { file -> - if (file != null && file.extension == "jar") { - val jar = JarFile(file) - val pluginYml = - jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull() - - if (pluginYml == null) { - logger.info("plugin.yml not found in jar " + jar.name + ", it will not be consider as a Plugin") - } else { - try { - val description = PluginDescription.readFromContent( - URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().let { - val res = it.inputStream.use { input -> - input.readBytes().encodeToString() - } - - // 关闭jarFile,解决热更新插件问题 - (it as JarURLConnection).jarFile.close() - res - }, file - ) - if (!isPluginLoaded(file, description.name)) { - pluginsFound[description.name] = description - pluginsLocation[description.name] = file - } - } catch (e: Exception) { - logger.info(e) - } - } - } - } - return FindPluginsResult(pluginsLocation, pluginsFound) - } - - internal fun loadPluginsImpl(clear: Boolean = true) { - logger.info("""开始加载${pluginsPath}下的插件""") - val findPluginsResult = findPlugins() - val pluginsFound = findPluginsResult.pluginsFound - val pluginsLocation = findPluginsResult.pluginsLocation - - //不仅要解决A->B->C->A, 还要解决A->B->A->A - fun checkNoCircularDepends( - target: PluginDescription, - needDepends: List, - existDepends: MutableList - ) { - - if (!target.noCircularDepend) { - return - } - - existDepends.add(target.name) - - if (needDepends.any { existDepends.contains(it) }) { - target.noCircularDepend = false - } - - existDepends.addAll(needDepends) - - needDepends.forEach { - if (pluginsFound.containsKey(it)) { - checkNoCircularDepends(pluginsFound[it]!!, pluginsFound[it]!!.depends, existDepends) - } - } - } - - pluginsFound.values.forEach { - checkNoCircularDepends(it, it.depends, mutableListOf()) - } - - //load plugin individually - fun loadPlugin(description: PluginDescription): Boolean { - if (!description.noCircularDepend) { - logger.error("Failed to load plugin " + description.name + " because it has circular dependency") - return false - } - - if (description.loaded || nameToPluginBaseMap.containsKey(description.name)) { - return true - } - - description.depends.forEach { dependent -> - if (!pluginsFound.containsKey(dependent)) { - logger.error("Failed to load plugin " + description.name + " because it need " + dependent + " as dependency") - return false - } - val depend = pluginsFound[dependent]!! - - if (!loadPlugin(depend)) {//先加载depend - logger.error("Failed to load plugin " + description.name + " because " + dependent + " as dependency failed to load") - return false - } - } - - logger.info("loading plugin " + description.name) - - val jarFile = pluginsLocation[description.name]!! - val pluginClass = try { - pluginsLoader.loadPluginMainClassByJarFile(description.name, description.basePath, jarFile) - } catch (e: ClassNotFoundException) { - pluginsLoader.loadPluginMainClassByJarFile(description.name, "${description.basePath}Kt", jarFile) - } - - val subClass = pluginClass.asSubclass(PluginBase::class.java) - - lastPluginName = description.name - val plugin: PluginBase = - subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().apply { - kotlin.runCatching { - this.isAccessible = true - } - }.newInstance() - plugin.dataFolder // initialize right now - - description.loaded = true - logger.info("successfully loaded plugin " + description.name + " version " + description.version + " by " + description.author) - logger.info(description.info) - - nameToPluginBaseMap[description.name] = plugin - pluginDescriptions[description.name] = description - plugin.pluginName = description.name - pluginsSequence.addLast(plugin)//按照实际加载顺序加入队列 - return true - } - - - if (clear) { - //清掉优先级队列, 来重新填充 - pluginsSequence.clear() - } - - pluginsFound.values.forEach { - try { - // 尝试加载插件 - loadPlugin(it) - } catch (e: Throwable) { - pluginsLoader.remove(it.name) - when (e) { - is ClassCastException -> logger.error( - "failed to load plugin " + it.name + " , Main class does not extends PluginBase", - e - ) - is ClassNotFoundException -> logger.error( - "failed to load plugin " + it.name + " , Main class not found under " + it.basePath, - e - ) - is NoClassDefFoundError -> logger.error( - "failed to load plugin " + it.name + " , dependent class not found.", - e - ) - else -> logger.error("failed to load plugin " + it.name, e) - } - } - } - - - pluginsSequence.forEach { - try { - it.load() - } catch (ignored: Throwable) { - logger.info(ignored) - logger.info(it.pluginName + " failed to load, disabling it") - logger.info(it.pluginName + " 推荐立即删除/替换并重启") - if (ignored is CancellationException) { - disablePlugin(it, ignored) - } else { - disablePlugin(it) - } - } - } - - pluginsSequence.forEach { - try { - it.enable() - } catch (ignored: Throwable) { - logger.info(ignored) - logger.info(it.pluginName + " failed to enable, disabling it") - logger.info(it.pluginName + " 推荐立即删除/替换并重启") - if (ignored is CancellationException) { - disablePlugin(it, ignored) - } else { - disablePlugin(it) - } - } - } - - logger.info("""加载了${nameToPluginBaseMap.size}个插件""") - } - - private fun disablePlugin( - plugin: PluginBase, - exception: CancellationException? = null - ) { - CommandManager.clearPluginCommands(plugin) - plugin.disable(exception) - nameToPluginBaseMap.remove(plugin.pluginName) - pluginDescriptions.remove(plugin.pluginName) - pluginsLoader.remove(plugin.pluginName) - pluginsSequence.remove(plugin) - } - - - /** - * 根据插件名字找Jar的文件 - * null => 没找到 - * 这里的url的jarFile没关,热更新插件可能出事 - */ - internal fun getJarFileByName(pluginName: String): File? { - File(pluginsPath).listFiles()?.forEach { file -> - if (file != null && file.extension == "jar") { - val jar = JarFile(file) - val pluginYml = - jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull() - if (pluginYml != null) { - val description = - PluginDescription.readFromContent( - URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use { - it.readBytes().encodeToString() - }, file - ) - if (description.name.toLowerCase() == pluginName.toLowerCase()) { - return file - } - } - } - } - return null - } - - - /** - * 根据插件名字找Jar中的文件 - * null => 没找到 - * 这里的url的jarFile没关,热更新插件可能出事 - */ - internal fun getFileInJarByName(pluginName: String, toFind: String): InputStream? { - val jarFile = getJarFileByName(pluginName) ?: return null - val jar = JarFile(jarFile) - val toFindFile = - jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null - return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream - } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt deleted file mode 100644 index fb2f5b648..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.pure - -import net.mamoe.mirai.console.MiraiConsole -import kotlin.concurrent.thread - -class MiraiConsolePureLoader { - companion object { - @JvmStatic - fun load( - coreVersion: String, - consoleVersion: String - ) { - MiraiConsole.start( - MiraiConsoleUIPure(), - coreVersion, - consoleVersion - ) - Runtime.getRuntime().addShutdownHook(thread(start = false) { - MiraiConsole.stop() - }) - } - } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt deleted file mode 100644 index 0688b0886..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.pure - -import kotlinx.coroutines.delay -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.CommandManager -import net.mamoe.mirai.console.command.ConsoleCommandSender -import net.mamoe.mirai.console.utils.MiraiConsoleUI -import net.mamoe.mirai.utils.DefaultLoginSolver -import net.mamoe.mirai.utils.LoginSolver -import net.mamoe.mirai.utils.SimpleLogger.LogPriority -import java.text.SimpleDateFormat -import java.util.* -import kotlin.concurrent.thread - - -class MiraiConsoleUIPure : MiraiConsoleUI { - private var requesting = false - private var requestStr = "" - - @Suppress("unused") - companion object { - // ANSI color codes - const val COLOR_RED = "\u001b[38;5;196m" - const val COLOR_CYAN = "\u001b[38;5;87m" - const val COLOR_GREEN = "\u001b[38;5;82m" - - // use a dark yellow(more like orange) instead of light one to save Solarized-light users - const val COLOR_YELLOW = "\u001b[38;5;220m" - const val COLOR_GREY = "\u001b[38;5;244m" - const val COLOR_BLUE = "\u001b[38;5;27m" - const val COLOR_NAVY = "\u001b[38;5;24m" // navy uniform blue - const val COLOR_PINK = "\u001b[38;5;207m" - const val COLOR_RESET = "\u001b[39;49m" - } - - init { - thread(name = "Mirai Console Input Thread") { - while (true) { - val input = readLine() ?: return@thread - if (requesting) { - requestStr = input - requesting = false - } else { - CommandManager.runCommand(ConsoleCommandSender, input) - } - } - } - } - - val sdf by lazy { - SimpleDateFormat("HH:mm:ss") - } - - override fun pushLog(identity: Long, message: String) { - println("\u001b[0m " + sdf.format(Date()) + " $message") - } - - override fun pushLog(priority: LogPriority, identityStr: String, identity: Long, message: String) { - var priorityStr = "[${priority.name}]" - /* - * 通过ANSI控制码添加颜色 - * 更多的颜色定义在 [MiraiConsoleUIPure] 的 companion - */ - priorityStr = when (priority) { - LogPriority.ERROR - -> COLOR_RED + priorityStr + COLOR_RESET - - LogPriority.WARNING - -> COLOR_RED + priorityStr + COLOR_RESET - - LogPriority.VERBOSE - -> COLOR_NAVY + priorityStr + COLOR_RESET - - LogPriority.DEBUG - -> COLOR_PINK + priorityStr + COLOR_RESET - - else -> priorityStr - } - println("\u001b[0m " + sdf.format(Date()) + " $priorityStr $identityStr ${message + COLOR_RESET}") - } - - override fun prePushBot(identity: Long) { - - } - - override fun pushBot(bot: Bot) { - - } - - override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) { - - } - - override suspend fun requestInput(hint:String): String { - if(hint.isNotEmpty()){ - println("\u001b[0m " + sdf.format(Date()) + COLOR_PINK + " $hint") - } - requesting = true - while (true) { - delay(50) - if (!requesting) { - return requestStr - } - } - } - - override fun pushBotAdminStatus(identity: Long, admins: List) { - - } - - override fun createLoginSolver(): LoginSolver { - return DefaultLoginSolver( - input = suspend { - requestInput("") - } - ) - } - -} - - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/scheduler/SchedulerTask.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/scheduler/SchedulerTask.kt deleted file mode 100644 index c6a1e996f..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/scheduler/SchedulerTask.kt +++ /dev/null @@ -1,149 +0,0 @@ -package net.mamoe.mirai.console.scheduler - -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import net.mamoe.mirai.console.plugins.PluginBase -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import java.util.function.Supplier -import kotlin.coroutines.CoroutineContext - - -/** - * 作为Java插件开发者, 你应该使用PluginScheduler - * 他们使用kotlin更高效的协程实现,并在API上对java有很高的亲和度 - * 且可以保证在PluginBase关闭的时候结束所有任务 - * - * 你应该使用SchedulerTaskManager获取PluginScheduler, 或直接通过PluginBase获取 - */ - -class PluginScheduler(_coroutineContext: CoroutineContext) : CoroutineScope { - override val coroutineContext: CoroutineContext = _coroutineContext + SupervisorJob(_coroutineContext[Job]) - - - class RepeatTaskReceipt(@Volatile var cancelled: Boolean = false) - - /** - * 新增一个 Repeat Task (定时任务) - * - * 这个 Runnable 会被每 [intervalMs] 调用一次(不包含 [runnable] 执行时间) - * - * 使用返回的 [RepeatTaskReceipt], 可以取消这个定时任务 - */ - fun repeat(runnable: Runnable, intervalMs: Long): RepeatTaskReceipt { - val receipt = RepeatTaskReceipt() - - this.launch { - while (isActive && (!receipt.cancelled)) { - withContext(Dispatchers.IO) { - runnable.run() - } - delay(intervalMs) - } - } - - return receipt - } - - /** - * 新增一个 Delay Task (延迟任务) - * - * 在延迟 [delayMs] 后执行 [runnable] - * - * 作为 Java 使用者, 你要注意可见性, 原子性 - */ - fun delay(runnable: Runnable, delayMs: Long) { - this.launch { - delay(delayMs) - withContext(Dispatchers.IO) { - runnable.run() - } - } - } - - /** - * 异步执行一个任务, 最终返回 [Future], 与 Java 使用方法无异, 但效率更高且可以在插件关闭时停止 - */ - fun async(supplier: Supplier): Future { - return AsyncResult( - this.async { - withContext(Dispatchers.IO) { - supplier.get() - } - } - ) - } - - /** - * 异步执行一个任务, 没有返回 - */ - fun async(runnable: Runnable) { - this.launch { - withContext(Dispatchers.IO) { - runnable.run() - } - } - } - -} - - -/** - * 这个类作为 Java 与 Kotlin 的桥接 - * 用 Java 的 interface 进行了 Kotlin 的实现 - * 使得 Java 开发者可以使用 Kotlin 的协程 [CoroutineScope.async] - * 具体使用方法与 Java 的 [Future] 没有区别 - */ -class AsyncResult( - private val deferred: Deferred -) : Future { - - override fun isDone(): Boolean { - return deferred.isCompleted - } - - override fun get(): T { - return runBlocking { - deferred.await() - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - override fun get(p0: Long, p1: TimeUnit): T { - return runBlocking { - withTimeoutOrNull(p1.toMillis(p0)) { - deferred.await() - } ?: throw TimeoutException() - } - } - - override fun cancel(p0: Boolean): Boolean { - deferred.cancel() - return true - } - - override fun isCancelled(): Boolean { - return deferred.isCancelled - } -} - - -internal object SchedulerTaskManagerInstance { - private val schedulerTaskManagerInstance = mutableMapOf() - - private val mutex = Mutex() - - fun getPluginScheduler(pluginBase: PluginBase): PluginScheduler { - runBlocking { - mutex.withLock { - if (!schedulerTaskManagerInstance.containsKey(pluginBase)) { - schedulerTaskManagerInstance[pluginBase] = PluginScheduler(pluginBase.coroutineContext) - } - } - } - return schedulerTaskManagerInstance[pluginBase]!! - } -} - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt deleted file mode 100644 index 1c01500c2..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.utils - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.plugins.* -import net.mamoe.mirai.console.utils.BotManagers.BOT_MANAGERS -import net.mamoe.mirai.contact.User -import java.io.File - - -/** - * 判断此用户是否为 console 管理员 - */ -val User.isManager: Boolean - get() = this.bot.managers.contains(this.id) - -@OptIn(ToBeRemoved::class) -internal object BotManagers { - val config = File("${MiraiConsole.path}/bot.yml").loadAsConfig() - val BOT_MANAGERS: ConfigSection by config.withDefaultWriteSave { ConfigSectionImpl() } -} - -@JvmName("addManager") -@JvmSynthetic -@Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) -fun Bot.addManagerDeprecated(long: Long) { - addManager(long) -} - -@OptIn(ToBeRemoved::class) -internal fun Bot.addManager(long: Long): Boolean { - BOT_MANAGERS.putIfAbsent(this.id.toString(), mutableListOf()) - BOT_MANAGERS[this.id.toString()] = - (BOT_MANAGERS.getLongList(this.id.toString()) as MutableList).apply { - if (contains(long)) return@addManager false - add(long) - } - BotManagers.config.save() - return true -} - -@OptIn(ToBeRemoved::class) -fun Bot.removeManager(long: Long) { - BOT_MANAGERS.putIfAbsent(this.id.toString(), mutableListOf()) - BOT_MANAGERS[this.id.toString()] = - (BOT_MANAGERS.getLongList(this.id.toString()) as MutableList).apply { remove(long) } - BotManagers.config.save() -} - -val Bot.managers: List - @OptIn(ToBeRemoved::class) - get() { - BOT_MANAGERS.putIfAbsent(this.id.toString(), mutableListOf()) - return BOT_MANAGERS.getLongList(this.id.toString()) - } - -fun Bot.checkManager(long: Long): Boolean { - return this.managers.contains(long) -} - - -fun getBotManagers(bot: Bot): List { - return bot.managers -} diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt deleted file mode 100644 index 8295c7012..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/ConsoleInput.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.utils - -import kotlinx.coroutines.* -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.plugins.PluginBase -import java.util.concurrent.Executors - -@Suppress("unused") -object ConsoleInput { - private val inputDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() - - /** - * 向用户索要一个Input - * 你需要提供一个hint(提示)并等待获取一个结果 - * 具体索要方式将根据frontend不同而不同 - * 如弹出框,或一行字 - */ - suspend fun requestInput( - hint:String - ):String{ - return withContext(inputDispatcher) { - MiraiConsole.frontEnd.requestInput(hint) - } - } - - fun requestInputBlocking(hint:String):String = runBlocking { requestInput(hint) } - - /** - * asnyc获取 - */ - fun requestInputAsync( - pluginBase: PluginBase, - hint: String - ):Deferred{ - return pluginBase.async { - requestInput(hint) - } - } - - suspend fun MiraiConsole.requestInput(hint:String):String = requestInput(hint) - - suspend fun PluginBase.requestInputAsync(hint:String):Deferred = requestInputAsync(hint) -} - - - - diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt deleted file mode 100644 index 624685049..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.utils - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.center.CuiPluginCenter -import net.mamoe.mirai.console.center.PluginCenter -import net.mamoe.mirai.utils.LoginSolver -import net.mamoe.mirai.utils.MiraiInternalAPI -import net.mamoe.mirai.utils.SimpleLogger.LogPriority - -/** - * 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console 层 - * 需要保证线程安全 - */ -@MiraiInternalAPI -interface MiraiConsoleUI { - /** - * 提供 [PluginCenter] - */ - val pluginCenter: PluginCenter get() = CuiPluginCenter - - /** - * 让 UI 层展示一条 log - * - * identity:log 所属的 screen, Main=0; Bot=Bot.uin - */ - fun pushLog( - identity: Long, - message: String - ) - - fun pushLog( - priority: LogPriority, - identityStr: String, - identity: Long, - message: String - ) - - /** - * 让 UI 层准备接受新增的一个BOT - */ - fun prePushBot( - identity: Long - ) - - /** - * 让 UI 层接受一个新的bot - * */ - fun pushBot( - bot: Bot - ) - - fun pushVersion( - consoleVersion: String, - consoleBuild: String, - coreVersion: String - ) - - /** - * 让 UI 层提供一个输入, 相当于 [readLine] - */ - suspend fun requestInput(hint: String): String - - - /** - * 让 UI 层更新 bot 管理员的数据 - */ - fun pushBotAdminStatus( - identity: Long, - admins: List - ) - - /** - * 由 UI 层创建一个 [LoginSolver] - */ - fun createLoginSolver(): LoginSolver - -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt deleted file mode 100644 index 5562465be..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Utils.kt +++ /dev/null @@ -1,161 +0,0 @@ -package net.mamoe.mirai.console.utils -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.Member -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * 执行N次 builder - * 成功一次就会结束 - * 否则就会throw - */ -@OptIn(ExperimentalContracts::class) -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -inline fun retryCatching(n: Int, block: () -> R): Result { - contract { - callsInPlace(block, InvocationKind.AT_LEAST_ONCE) - } - require(n >= 0) { "param n for retryCatching must not be negative" } - var exception: Throwable? = null - repeat(n){ - try { - return Result.success(block()) - } catch (e: Throwable) { - exception?.addSuppressedMirai(e) - exception = e - } - } - return Result.failure(exception!!) -} - -@OptIn(ExperimentalContracts::class) -inline fun tryNTimes(n: Int = 2, block: () -> T):T { - contract { - callsInPlace(block, InvocationKind.AT_LEAST_ONCE) - } - require(n >= 0) { "param n for tryNTimes must not be negative" } - var last:Exception? = null - repeat(n){ - try { - return block.invoke() - }catch (e:Exception){ - last = e - } - } - - //给我编译 - - throw last!! -} - -@PublishedApi -internal fun Throwable.addSuppressedMirai(e: Throwable) { - if (e === this) { - return - } - kotlin.runCatching { - this.addSuppressed(e) - } -} - - -/** - * 两个字符串的近似值 - * 要求前面完全一致 - * 如 - * XXXXXYYYYY.fuzzyCompare(XXXXXYYY) = 0.8 - * XXXXXYYYYY.fuzzyCompare(XXXXXYYYZZ) = 0.8 - */ - -internal fun String.fuzzyCompare(target: String): Double { - var step = 0 - if (this == target) { - return 1.0 - } - if (target.length > this.length) { - return 0.0 - } - for (i in this.indices) { - if (target.length == i) { - step-- - }else { - if (this[i] != target[i]) { - break - } - step++ - } - } - - if(step == this.length-1){ - return 1.0 - } - return step.toDouble()/this.length -} - -/** - * 模糊搜索一个List中index最接近target的东西 - */ -internal inline fun Collection.fuzzySearch( - target: String, - index: (T) -> String -): T? { - if (this.isEmpty()) { - return null - } - var potential: T? = null - var rate = 0.0 - this.forEach { - val thisIndex = index(it) - if(thisIndex == target){ - return it - } - with(thisIndex.fuzzyCompare(target)) { - if (this > rate) { - rate = this - potential = it - } - } - } - return potential -} - -/** - * 模糊搜索一个List中index最接近target的东西 - * 并且确保target是唯一的 - * 如搜索index为XXXXYY list中同时存在XXXXYYY XXXXYYYY 将返回null - */ -internal inline fun Collection.fuzzySearchOnly( - target: String, - index: (T) -> String -): T? { - if (this.isEmpty()) { - return null - } - var potential: T? = null - var rate = 0.0 - var collide = 0 - this.forEach { - with(index(it).fuzzyCompare(target)) { - if (this > rate) { - rate = this - potential = it - } - if(this == 1.0){ - collide++ - } - if(collide > 1){ - return null//collide - } - } - } - return potential -} - - -internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { - return this.members.fuzzySearchOnly(nameCardTarget) { - it.nameCard - } -} \ No newline at end of file diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Value.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Value.kt deleted file mode 100644 index 85a3ba822..000000000 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/Value.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.mamoe.mirai.console.utils - -import net.mamoe.mirai.utils.MiraiExperimentalAPI - -/** - * A Value - * the input type of this Value is T while the output is V - */ -@MiraiExperimentalAPI -abstract class Value { - operator fun invoke(): V = get() - - abstract fun get(): V - - abstract fun set(t: T) -} - - -/** - * This value can be used as a Config Value - */ -@MiraiExperimentalAPI -interface ConfigValue - - -/** - * A simple value - * the input type is same as output value - */ - -@MiraiExperimentalAPI -open class SimpleValue( - var value: T -) : Value() { - override fun get() = this.value - - override fun set(t: T) { - this.value = t - } -} - -@MiraiExperimentalAPI -open class NullableSimpleValue( - value: T? = null -) : SimpleValue( - value -) { - fun isNull() = value == null -} - diff --git a/mirai-console/src/test/kotlin/StringFuzzyTest.kt b/mirai-console/src/test/kotlin/StringFuzzyTest.kt deleted file mode 100644 index 58177caeb..000000000 --- a/mirai-console/src/test/kotlin/StringFuzzyTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -import net.mamoe.mirai.console.utils.fuzzySearch -import net.mamoe.mirai.console.utils.fuzzySearchOnly - -class Him188(val name:String){ - override fun toString(): String { - return name - } -} - -fun main(){ - val list = listOf( - Him188("111122"), - Him188("H1hsncm"), - Him188("Ahsndb1"), - Him188("Him188"), - Him188("aisb11j2"), - Him188("aisndnme"), - Him188("a9su102"), - Him188("nmsl"), - Him188("Him1888"), - Him188("Him18887") - ) - val s1 = list.fuzzySearch("Him1888"){ - it.name - } - val s2 = list.fuzzySearchOnly("Him1888"){ - it.name - } - println("S1: $s1") - println("S2: $s2") -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index e1df949be..000000000 --- a/settings.gradle +++ /dev/null @@ -1,56 +0,0 @@ -pluginManagement { - resolutionStrategy { - eachPlugin { - switch (requested.id.id) { - case "org.jetbrains.kotlin.multiplatform": useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}"); break - case "com.android.library": useModule("com.android.tools.build:gradle:${requested.version}"); break - case "com.jfrog.bintray": useModule("com.jfrog.bintray.gradle:gradle-bintray-plugin:${requested.version}") - } - } - } - - repositories { - mavenLocal() - jcenter() - google() - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" } - maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } - maven { url 'https://plugins.gradle.org/m2/' } - } -} - -rootProject.name = 'mirai-console' - -include(':mirai-console') -include(':mirai-console-terminal') - -try{ - def javaVersion = System.getProperty("java.version") - def versionPos = javaVersion.indexOf(".") - def javaVersionNum = javaVersion.substring(0, 1).toInteger() - - if (javaVersion.startsWith("1.")) { - javaVersionNum = javaVersion.substring(2, 3).toInteger() - } else { - if (versionPos==-1) versionPos = javaVersion.indexOf("-") - if (versionPos==-1){ - println("jdk version unknown") - }else{ - javaVersionNum = javaVersion.substring(0, versionPos).toInteger() - } - } - if (javaVersionNum >= 9) { - include(':mirai-console-graphical') - } else { - println("jdk版本为 "+ javaVersionNum) - println("当前使用的 JDK 版本为 ${System.getProperty("java.version")}, 请使用JDK 9以上版本引入模块 `:mirai-console-graphical`\n") - } - -}catch(Exception ignored){ - println("无法确定 JDK 版本, 将不会引入 `:mirai-console-graphical`") -} - - -enableFeaturePreview('GRADLE_METADATA') \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..4283675f6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,53 @@ +pluginManagement { + repositories { + mavenLocal() + jcenter() + maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") + mavenCentral() + } + + resolutionStrategy { + eachPlugin { + val version = requested.version + when (requested.id.id) { + "org.jetbrains.kotlin.jvm" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${version}") + "org.jetbrains.kotlin.plugin.serialization" -> useModule("org.jetbrains.kotlin:kotlin-serialization:${version}") + "com.jfrog.bintray" -> useModule("com.jfrog.bintray.gradle:gradle-bintray-plugin:$version") + } + } + } +} + +rootProject.name = "mirai-console" + +val disableOldFrontEnds = true + +fun includeProject(projectPath: String, path: String? = null) { + include(projectPath) + if (path != null) project(projectPath).projectDir = file(path) +} + +includeProject(":mirai-console", "backend/mirai-console") +includeProject(":mirai-console.codegen", "backend/codegen") +includeProject(":mirai-console-pure", "frontend/mirai-console-pure") + +@Suppress("ConstantConditionIf") +if (!disableOldFrontEnds) { + includeProject(":mirai-console-terminal", "frontend/mirai-console-terminal") + + val jdkVersion = kotlin.runCatching { + System.getProperty("java.version").let { v -> + v.toIntOrNull() ?: v.removePrefix("1.").substringBefore("-").toIntOrNull() + } + }.getOrNull() ?: -1 + + println("JDK version: $jdkVersion") + + if (jdkVersion >= 9) { + includeProject(":mirai-console-graphical", "frontend/mirai-console-graphical") + } else { + println("当前使用的 JDK 版本为 ${System.getProperty("java.version")}, 请使用 JDK 9 以上版本引入模块 `:mirai-console-graphical`\n") + } +} + +enableFeaturePreview("GRADLE_METADATA") \ No newline at end of file