mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-07 16:40:43 +08:00
Merge branch 'reborn'
# Conflicts: # README.md # buildSrc/src/main/kotlin/versions.kt # mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt # mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt
This commit is contained in:
commit
c4d98f1c9a
2
.github/workflows/cui.yml
vendored
2
.github/workflows/cui.yml
vendored
@ -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 }}
|
||||
|
||||
|
||||
|
2
.github/workflows/shadow.yml
vendored
2
.github/workflows/shadow.yml
vendored
@ -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 }}
|
||||
|
||||
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -43,4 +43,7 @@ keys.properties
|
||||
/plugins/
|
||||
|
||||
bintray.user.txt
|
||||
bintray.key.txt
|
||||
bintray.key.txt
|
||||
|
||||
token.txt
|
||||
/*/token.txt
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "frontend/mirai-android"]
|
||||
path = frontend/mirai-android
|
||||
url = https://github.com/mzdluo123/MiraiAndroid
|
@ -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;
|
||||
|
26
backend/codegen/build.gradle.kts
Normal file
26
backend/codegen/build.gradle.kts
Normal file
@ -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"))
|
||||
}
|
@ -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 ?: "<unnamed>") {
|
||||
override fun invoke(p1: String): String = block(p1)
|
||||
}
|
||||
}
|
||||
|
||||
class CodegenScope : MutableList<Replacer> 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<KtType>) {
|
||||
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<KtType>) {
|
||||
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<KtType>
|
||||
}
|
||||
|
||||
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<KtPrimitive>) = ktType.forEach { apply(it) }
|
||||
}
|
||||
|
||||
fun StringBuilder.appendKCode(@Language("kt") ktCode: String): StringBuilder = append(kCode(ktCode)).appendLine()
|
@ -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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = 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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = 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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = KtPrimitives + KtString
|
||||
|
||||
override fun StringBuilder.apply(ktType: KtType) {
|
||||
appendKCode(
|
||||
"""
|
||||
internal abstract class ${ktType.standardName}ValueImpl : ${ktType.standardName}Value, SerializerAwareValue<${ktType.standardName}>, KSerializer<Unit>, 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<Unit> 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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = 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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = 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<String>) = super.startIndependently()
|
||||
override val defaultInvokeArgs: List<KtType> = 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<String>) {
|
||||
ValueSettingCodegen::class.nestedClasses
|
||||
.filter { it.isSubclassOf(RegionCodegen::class) }
|
||||
.associateWith { kClass -> kClass.functions.find { it.name == "main" && it.hasAnnotation<JvmStatic>() } }
|
||||
.filter { it.value != null }
|
||||
.forEach { (kClass, entryPoint) ->
|
||||
println("---------------------------------------------")
|
||||
println("Running Codegen: ${kClass.simpleName}")
|
||||
entryPoint!!.call(kClass.objectInstance, arrayOf<String>())
|
||||
println("---------------------------------------------")
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -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 <T : Setting> Setting.value(default: T): Value<T> {
|
||||
require(this::class != default::class) {
|
||||
"Recursive nesting is prohibited"
|
||||
}
|
||||
return valueImpl(default).also {
|
||||
if (default is Setting.NestedSetting) {
|
||||
default.attachedValue = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : Setting> Setting.value(default: T, crossinline initializer: T.() -> Unit): Value<T> =
|
||||
value(default).also { it.value.apply(initializer) }
|
||||
|
||||
inline fun <reified T : Setting> Setting.value(default: List<T>): SettingListValue<T> = valueImpl(default)
|
||||
|
||||
@JvmName("valueMutable")
|
||||
inline fun <reified T : Setting> Setting.value(default: MutableList<T>): MutableSettingListValue<T> = valueImpl(default)
|
||||
|
||||
|
||||
inline fun <reified T : Setting> Setting.value(default: Set<T>): SettingSetValue<T> = valueImpl(default)
|
||||
|
||||
@JvmName("valueMutable")
|
||||
inline fun <reified T : Setting> Setting.value(default: MutableSet<T>): MutableSettingSetValue<T> = valueImpl(default)
|
||||
|
||||
/**
|
||||
* 创建一个只引用对象而不跟踪其属性的值.
|
||||
*
|
||||
* @param T 类型. 必须拥有 [kotlinx.serialization.Serializable] 注解 (因此编译器会自动生成序列化器)
|
||||
*/
|
||||
@DangerousReferenceOnlyValue
|
||||
@JvmName("valueDynamic")
|
||||
@LowPriorityInOverloadResolution
|
||||
inline fun <reified T : Any> Setting.value(default: T): Value<T> = 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()
|
||||
|
@ -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<Mutable${collectionName}<${number}>> = object : KSerializer<Mutable${collectionName}<${number}>> {
|
||||
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 <T : Setting> Setting.valueImpl(default: T): Value<T> {
|
||||
return object : SettingValue<T>() {
|
||||
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<T>{
|
||||
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"
|
||||
|
@ -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<T : Any> : ReadWriteProperty<Setting, T> {
|
||||
abstract var value: T
|
||||
|
||||
/**
|
||||
* 用于更新 [value] 的序列化器
|
||||
*/
|
||||
abstract val serializer: KSerializer<T>
|
||||
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<T : Any> : Value<T>()
|
||||
|
||||
sealed class NumberValue<T : Number> : Value<T>()
|
||||
"""
|
||||
)
|
||||
|
||||
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<T : Any> : Value<T>()
|
||||
"""
|
||||
)
|
||||
|
||||
// PRIMITIVE ARRAYS
|
||||
appendln(
|
||||
"""
|
||||
sealed class PrimitiveArrayValue<T : Any> : ArrayValue<T>()
|
||||
"""
|
||||
)
|
||||
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<E> : ArrayValue<Array<E>>() , Iterable<E>{
|
||||
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<E> : Value<${collectionName}<E>>(), ${collectionName}<E>
|
||||
"""
|
||||
)
|
||||
|
||||
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<T: Setting> internal constructor() : Value<${collectionName}<T>>(), ${collectionName}<T>
|
||||
"""
|
||||
)
|
||||
appendLine()
|
||||
}
|
||||
|
||||
// SETTING VALUE
|
||||
|
||||
appendln(
|
||||
"""
|
||||
abstract class SettingValue<T : Setting> internal constructor() : Value<T>()
|
||||
"""
|
||||
)
|
||||
|
||||
appendLine()
|
||||
|
||||
// MUTABLE LIST / MUTABLE SET
|
||||
for (collectionName in listOf("List", "Set")) {
|
||||
appendln(
|
||||
"""
|
||||
abstract class Mutable${collectionName}Value<T : Any> internal constructor() : Value<Mutable${collectionName}<Value<T>>>(), Mutable${collectionName}<T>
|
||||
"""
|
||||
)
|
||||
|
||||
appendLine()
|
||||
|
||||
for (number in (NUMBERS + OTHER_PRIMITIVES)) {
|
||||
appendln(
|
||||
"""
|
||||
abstract class Mutable${number}${collectionName}Value internal constructor() : Value<Mutable${collectionName}<${number}>>(), Mutable${collectionName}<${number}>
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
appendLine()
|
||||
// SETTING
|
||||
appendln(
|
||||
"""
|
||||
abstract class MutableSetting${collectionName}Value<T: Setting> internal constructor() : Value<Mutable${collectionName}<T>>(), Mutable${collectionName}<T>
|
||||
"""
|
||||
)
|
||||
appendLine()
|
||||
}
|
||||
|
||||
appendLine()
|
||||
// DYNAMIC
|
||||
|
||||
appendln(
|
||||
"""
|
||||
/**
|
||||
* 只引用这个对象, 而不跟踪其成员.
|
||||
* 仅适用于基础类型, 用于 mutable list/map 等情况; 或标注了 [Serializable] 的类.
|
||||
*/
|
||||
abstract class DynamicReferenceValue<T : Any> : Value<T>()
|
||||
"""
|
||||
)
|
||||
}
|
@ -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<KtType> {
|
||||
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" }
|
||||
}
|
87
backend/mirai-console/README.MD
Normal file
87
backend/mirai-console/README.MD
Normal file
@ -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 兼容性最高, 在其他都表现不佳的时候请使用</br>
|
||||
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 <b>Plugins</b>, tells the bot what to do
|
||||
#### Mirai Console 支持插件系统, 你可以自己开发或使用公开的插件来逻辑化机器人, 如群管
|
||||
<br>
|
||||
|
||||
#### download 下载
|
||||
#### how to get/write plugins 如何获取/写插件
|
||||
<br>
|
||||
<br>
|
||||
|
||||
### how to use(如何使用)
|
||||
#### how to run Mirai Console
|
||||
<ul>
|
||||
<li>download mirai-console.jar</li>
|
||||
<li>open command line/terminal</li>
|
||||
<li>create a folder and put mirai-console.jar in</li>
|
||||
<li>cd that folder</li>
|
||||
<li>"java -jar mirai-console.jar"</li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>下载mirai-console.jar</li>
|
||||
<li>打开终端</li>
|
||||
<li>在任何地方创建一个文件夹, 并放入mirai-console.jar</li>
|
||||
<li>在终端中打开该文件夹"cd"</li>
|
||||
<li>输入"java -jar mirai-console.jar"</li>
|
||||
</ul>
|
||||
|
||||
#### how to add plugins
|
||||
<ul>
|
||||
<li>After first time of running mirai console</li>
|
||||
<li>/plugins/folder will be created next to mirai-console.jar</li>
|
||||
<li>put plugin(.jar) into /plugins/</li>
|
||||
<li>restart mirai console</li>
|
||||
<li>checking logger and check if the plugin is loaded successfully</li>
|
||||
<li>if the plugin has it own Config file, it normally appears in /plugins/{pluginName}/</li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>在首次运行mirai console后</li>
|
||||
<li>mirai-console.jar 的同级会出现/plugins/文件夹</li>
|
||||
<li>将插件(.jar)放入/plugins/文件夹</li>
|
||||
<li>重启mirai console</li>
|
||||
<li>在开启后检查日志, 是否成功加载</li>
|
||||
<li>如该插件有配置文件, 配置文件一般会创建在/plugins/插件名字/ 文件夹下</li>
|
||||
</ul>
|
||||
|
||||
|
131
backend/mirai-console/build.gradle.kts
Normal file
131
backend/mirai-console/build.gradle.kts
Normal file
@ -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
|
@ -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>
|
||||
* 若原有指令 P, 其 {@link Command#getNames()} 为 'a', 'b', 'c'. <br>
|
||||
* 新指令 Q, 其 {@link Command#getNames()} 为 'b', 将会覆盖原指令 A 注册的 'b'.
|
||||
* <p>
|
||||
* 即注册完成后, 'a' 和 'c' 将会解析到指令 P, 而 'b' 会解析到指令 Q.
|
||||
* @return 若已有重名指令, 且 <code>override</code> 为 <code>false</code>, 返回 <code>false</code>; <br>
|
||||
* 若已有重名指令, 但 <code>override</code> 为 <code>true</code>, 覆盖原有指令并返回 <code>true</code>.
|
||||
*/
|
||||
public static boolean register(final @NotNull Command command, final boolean override) {
|
||||
Objects.requireNonNull(command, "command");
|
||||
return CommandManagerKt.register(command, override);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个指令, 已有重复名称的指令时返回 <code>false</code>
|
||||
*
|
||||
* @param command 指令实例
|
||||
* @return 若已有重名指令, 返回 <code>false</code>, 否则返回 <code>true</code>.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册这个指令. 若指令未注册, 返回 <code>false</code>.
|
||||
*/
|
||||
public static boolean unregister(final @NotNull Command command) {
|
||||
Objects.requireNonNull(command, "command");
|
||||
return CommandManagerKt.unregister(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册所有属于 <code>owner</code> 的指令
|
||||
*
|
||||
* @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 成功执行的指令, 在无匹配指令时返回 <code>null</code>
|
||||
* @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 成功执行的指令, 在无匹配指令时返回 <code>null</code>
|
||||
* @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));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析并执行一个指令, 获取详细的指令参数等信息.
|
||||
* <br />
|
||||
* 执行过程中产生的异常将不会直接抛出, 而会包装为 {@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));
|
||||
}
|
||||
}
|
@ -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<Long> getManagers(long botAccount) {
|
||||
Bot bot = Bot.getInstance(botAccount);
|
||||
return getManagers(bot);
|
||||
}
|
||||
|
||||
public static List<Long> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<PluginLoader<*, *>>
|
||||
|
||||
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<PluginLoader<*, *>> 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)
|
@ -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
|
||||
}
|
@ -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<PluginLoader<*, *>>
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<out Command> 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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
@ -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<out String>
|
||||
|
||||
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<out Any>)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Command] 的基础实现
|
||||
*/
|
||||
public abstract class AbstractCommand @JvmOverloads constructor(
|
||||
public final override val owner: CommandOwner,
|
||||
vararg names: String,
|
||||
description: String = "<no description available>",
|
||||
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<out String> =
|
||||
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<out Any>): Unit =
|
||||
sender.run { onCommand(args) }
|
||||
|
||||
/**
|
||||
* 主要指令名. 为 [Command.names] 的第一个元素.
|
||||
*/
|
||||
public val Command.primaryName: String get() = names[0]
|
@ -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<out Any>?
|
||||
|
||||
// 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<out Any>
|
||||
) : 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<out Any>
|
||||
) : 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
|
||||
}
|
@ -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')"
|
||||
}
|
||||
|
@ -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<Command> get() = InternalCommandManager.registeredCommands.filter { it.owner == this }
|
||||
|
||||
/**
|
||||
* 获取所有已经注册了指令列表.
|
||||
* @see JCommandManager.getRegisteredCommands Java 方法
|
||||
*/
|
||||
public val allRegisteredCommands: List<Command> 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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
@ -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<E : MessageEvent> : MessageEventExtensions<User, Contact> {
|
||||
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<FriendMessageEvent>, MessageEventExtensions<User, Contact> 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<GroupMessageEvent>, MessageEventExtensions<User, Contact> 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<TempMessageEvent>, MessageEventExtensions<User, Contact> by fromEvent {
|
||||
public override val subject: Contact get() = super.subject
|
||||
public override val bot: Bot get() = super.bot
|
||||
}
|
@ -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<out CommandPermission>)
|
||||
|
||||
/** 指令描述 */
|
||||
@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<out Any>) {
|
||||
matchSubCommand(args)?.parseAndExecute(this, args, true) ?: kotlin.run {
|
||||
defaultSubCommand.onCommand(this, args)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal override suspend fun CommandSender.onDefault(rawArgs: Array<out Any>) {
|
||||
sendMessage(usage)
|
||||
}
|
||||
|
||||
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
|
||||
get() = CompositeCommandSubCommandAnnotationResolver
|
||||
}
|
@ -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 = "<no usages given>",
|
||||
public override val description: String = "<no descriptions given>",
|
||||
public override val permission: CommandPermission = CommandPermission.Default,
|
||||
public override val prefixOptional: Boolean = false
|
||||
) : Command {
|
||||
public abstract override suspend fun CommandSender.onCommand(args: Array<out Any>)
|
||||
}
|
@ -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<SubCommandDescriptor>) {
|
||||
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<out Any>) = sendMessage(usage)
|
||||
|
||||
public final override suspend fun CommandSender.onCommand(args: Array<out Any>) {
|
||||
subCommands.single().parseAndExecute(this, args, false)
|
||||
}
|
||||
|
||||
internal final override val subCommandAnnotationResolver: SubCommandAnnotationResolver
|
||||
get() = SimpleCommandSubCommandAnnotationResolver
|
||||
}
|
@ -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<out T : Any> {
|
||||
public fun parse(raw: String, sender: CommandSender): T
|
||||
|
||||
@JvmDefault
|
||||
public fun parse(raw: SingleMessage, sender: CommandSender): T = parse(raw.content, sender)
|
||||
}
|
||||
|
||||
public fun <T : Any> CommandArgParser<T>.parse(raw: Any, sender: CommandSender): T {
|
||||
contract {
|
||||
returns() implies (raw is String || raw is SingleMessage)
|
||||
}
|
||||
|
||||
return when (raw) {
|
||||
is String -> parse(raw, sender)
|
||||
is SingleMessage -> parse(raw, sender)
|
||||
else -> throw IllegalArgumentException("Illegal raw argument type: ${raw::class.qualifiedName}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@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 <T : Any> CommandArgParser(
|
||||
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
||||
): CommandArgParser<T> = object : CommandArgParser<T> {
|
||||
override fun parse(raw: String, sender: CommandSender): T = stringParser(raw, sender)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建匿名 [CommandArgParser]
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
@JvmSynthetic
|
||||
public inline fun <T : Any> CommandArgParser(
|
||||
crossinline stringParser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T,
|
||||
crossinline messageParser: CommandArgParser<T>.(m: SingleMessage, sender: CommandSender) -> T
|
||||
): CommandArgParser<T> = object : CommandArgParser<T> {
|
||||
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)
|
@ -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<Int> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Int =
|
||||
raw.toIntOrNull() ?: illegalArgument("无法解析 $raw 为整数")
|
||||
}
|
||||
|
||||
public object LongArgParser : CommandArgParser<Long> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Long =
|
||||
raw.toLongOrNull() ?: illegalArgument("无法解析 $raw 为长整数")
|
||||
}
|
||||
|
||||
public object ShortArgParser : CommandArgParser<Short> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Short =
|
||||
raw.toShortOrNull() ?: illegalArgument("无法解析 $raw 为短整数")
|
||||
}
|
||||
|
||||
public object ByteArgParser : CommandArgParser<Byte> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Byte =
|
||||
raw.toByteOrNull() ?: illegalArgument("无法解析 $raw 为字节")
|
||||
}
|
||||
|
||||
public object DoubleArgParser : CommandArgParser<Double> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Double =
|
||||
raw.toDoubleOrNull() ?: illegalArgument("无法解析 $raw 为小数")
|
||||
}
|
||||
|
||||
public object FloatArgParser : CommandArgParser<Float> {
|
||||
public override fun parse(raw: String, sender: CommandSender): Float =
|
||||
raw.toFloatOrNull() ?: illegalArgument("无法解析 $raw 为小数")
|
||||
}
|
||||
|
||||
public object StringArgParser : CommandArgParser<String> {
|
||||
public override fun parse(raw: String, sender: CommandSender): String {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
public object BooleanArgParser : CommandArgParser<Boolean> {
|
||||
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<Bot> {
|
||||
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<Friend> {
|
||||
//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<Group> {
|
||||
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<Member> {
|
||||
//后台: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T : Any>(
|
||||
val klass: KClass<T>,
|
||||
val parser: CommandArgParser<T>
|
||||
)
|
||||
|
||||
public operator fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>?
|
||||
|
||||
public fun toList(): List<ParserPair<*>>
|
||||
|
||||
/**
|
||||
* 内建的默认 [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 <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? = replacer[klass] ?: this@plus[klass]
|
||||
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 [this] 与 [replacer], [replacer] 将会替换 [this] 中重复的 parser.
|
||||
*/
|
||||
public operator fun CommandParserContext.plus(replacer: List<ParserPair<*>>): CommandParserContext {
|
||||
if (replacer.isEmpty()) return this
|
||||
if (this == EmptyCommandParserContext) return CustomCommandParserContext(replacer)
|
||||
return object : CommandParserContext {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? =
|
||||
replacer.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgParser<T>? ?: this@plus[klass]
|
||||
|
||||
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
public open class CustomCommandParserContext(public val list: List<ParserPair<*>>) : CommandParserContext {
|
||||
|
||||
override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? =
|
||||
this.list.firstOrNull { klass.isSubclassOf(it.klass) }?.parser as CommandArgParser<T>?
|
||||
|
||||
override fun toList(): List<ParserPair<*>> {
|
||||
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<ParserPair<*>> by mutableListOf() {
|
||||
@JvmName("add")
|
||||
public inline infix fun <T : Any> KClass<T>.with(parser: CommandArgParser<T>): ParserPair<*> =
|
||||
ParserPair(this, parser).also { add(it) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@JvmSynthetic
|
||||
@LowPriorityInOverloadResolution
|
||||
public inline infix fun <T : Any> KClass<T>.with(
|
||||
crossinline parser: CommandArgParser<T>.(s: String, sender: CommandSender) -> T
|
||||
): ParserPair<*> = ParserPair(this, CommandArgParser(parser)).also { add(it) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@JvmSynthetic
|
||||
public inline infix fun <T : Any> KClass<T>.with(
|
||||
crossinline parser: CommandArgParser<T>.(s: String) -> T
|
||||
): ParserPair<*> = ParserPair(this, CommandArgParser { s: String, _: CommandSender -> parser(s) }).also { add(it) }
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun <reified T : Any> add(parser: CommandArgParser<T>): ParserPair<*> =
|
||||
ParserPair(T::class, parser).also { add(it) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@ConsoleExperimentalAPI
|
||||
@JvmSynthetic
|
||||
public inline infix fun <reified T : Any> add(
|
||||
crossinline parser: CommandArgParser<*>.(s: String) -> T
|
||||
): ParserPair<*> = T::class with CommandArgParser { s: String, _: CommandSender -> parser(s) }
|
||||
|
||||
/**
|
||||
* 添加一个指令解析器
|
||||
*/
|
||||
@ConsoleExperimentalAPI
|
||||
@JvmSynthetic
|
||||
@LowPriorityInOverloadResolution
|
||||
public inline infix fun <reified T : Any> add(
|
||||
crossinline parser: CommandArgParser<*>.(s: String, sender: CommandSender) -> T
|
||||
): ParserPair<*> = T::class with CommandArgParser(parser)
|
||||
}
|
||||
|
||||
|
||||
@PublishedApi
|
||||
internal inline fun <T, K> List<T>.distinctByReversed(selector: (T) -> K): List<T> {
|
||||
val set = HashSet<K>()
|
||||
val list = ArrayList<T>()
|
||||
for (i in this.indices.reversed()) {
|
||||
val element = this[i]
|
||||
if (set.add(element.let(selector))) {
|
||||
list.add(element)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
@ -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<T : Any>(
|
||||
/**
|
||||
* 参数名. 不允许重复.
|
||||
*/
|
||||
val name: String,
|
||||
/**
|
||||
* 参数类型. 将从 [CompositeCommand.context] 中寻找 [CommandArgParser] 解析.
|
||||
*/
|
||||
val type: KClass<T> // exact type
|
||||
) {
|
||||
constructor(name: String, type: KClass<T>, parser: CommandArgParser<T>) : this(name, type) {
|
||||
this._overrideParser = parser
|
||||
}
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@JvmField
|
||||
internal var _overrideParser: CommandArgParser<T>? = null
|
||||
|
||||
|
||||
/**
|
||||
* 覆盖的 [CommandArgParser].
|
||||
*
|
||||
* 如果非 `null`, 将不会从 [CommandParserContext] 寻找 [CommandArgParser]
|
||||
*/
|
||||
val overrideParser: CommandArgParser<T>? get() = _overrideParser
|
||||
}
|
||||
|
@ -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<CompositeCommand.SubCommand>()
|
||||
override fun getSubCommandNames(function: KFunction<*>): Array<out String> =
|
||||
function.findAnnotation<CompositeCommand.SubCommand>()!!.value
|
||||
}
|
||||
|
||||
internal object SimpleCommandSubCommandAnnotationResolver :
|
||||
AbstractReflectionCommand.SubCommandAnnotationResolver {
|
||||
override fun hasAnnotation(function: KFunction<*>) = function.hasAnnotation<SimpleCommand.Handler>()
|
||||
override fun getSubCommandNames(function: KFunction<*>): Array<out String> = arrayOf("")
|
||||
}
|
||||
|
||||
internal abstract class AbstractReflectionCommand @JvmOverloads constructor(
|
||||
owner: CommandOwner,
|
||||
names: Array<out String>,
|
||||
description: String = "<no description available>",
|
||||
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 = "<not yet initialized>"
|
||||
|
||||
override val usage: String // initialized by subCommand reflection
|
||||
get() = _usage
|
||||
|
||||
abstract suspend fun CommandSender.onDefault(rawArgs: Array<out Any>)
|
||||
|
||||
internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
|
||||
DefaultSubCommandDescriptor(
|
||||
"",
|
||||
permission,
|
||||
onCommand = block2 { sender: CommandSender, args: Array<out Any> ->
|
||||
sender.onDefault(args)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal open fun checkSubCommand(subCommands: Array<SubCommandDescriptor>) {
|
||||
|
||||
}
|
||||
|
||||
interface SubCommandAnnotationResolver {
|
||||
fun hasAnnotation(function: KFunction<*>): Boolean
|
||||
fun getSubCommandNames(function: KFunction<*>): Array<out String>
|
||||
}
|
||||
|
||||
internal val subCommands: Array<SubCommandDescriptor> 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<Array<String>, SubCommandDescriptor> by lazy {
|
||||
kotlin.run {
|
||||
val map = LinkedHashMap<Array<String>, 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<out Any>) -> Unit
|
||||
)
|
||||
|
||||
internal class SubCommandDescriptor(
|
||||
val names: Array<out String>,
|
||||
val params: Array<CommandParam<*>>,
|
||||
val description: String,
|
||||
val permission: CommandPermission,
|
||||
val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean,
|
||||
val context: CommandParserContext,
|
||||
val usage: String
|
||||
) {
|
||||
internal suspend inline fun parseAndExecute(
|
||||
sender: CommandSender,
|
||||
argsWithSubCommandNameNotRemoved: Array<out Any>,
|
||||
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<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
|
||||
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any>? {
|
||||
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<out Any>): 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 <T> Array<T>.contentEqualsOffset(other: Array<out Any>, 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<String> = split(' ').filterNot { it.isBlank() }.toTypedArray()
|
||||
|
||||
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
|
||||
val list = ArrayList<Any>()
|
||||
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 <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
|
||||
findAnnotation<T>() != null
|
||||
|
||||
internal inline fun <T : Any> KClass<out T>.getInstance(): T {
|
||||
return this.objectInstance ?: this.createInstance()
|
||||
}
|
||||
|
||||
internal val KClass<*>.qualifiedNameOrTip: String get() = this.qualifiedName ?: "<anonymous class>"
|
||||
|
||||
internal fun AbstractReflectionCommand.createSubCommand(
|
||||
function: KFunction<*>,
|
||||
context: CommandParserContext
|
||||
): AbstractReflectionCommand.SubCommandDescriptor {
|
||||
val notStatic = !function.hasAnnotation<JvmStatic>()
|
||||
val overridePermission = function.findAnnotation<CompositeCommand.Permission>()//optional
|
||||
val subDescription =
|
||||
function.findAnnotation<CompositeCommand.Description>()?.value ?: "<no description available>"
|
||||
|
||||
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<CompositeCommand.Name>()?.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<out Any> ->
|
||||
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<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
|
||||
return block
|
||||
}
|
||||
|
||||
private fun block2(block: suspend (CommandSender, Array<out Any>) -> Unit): suspend (CommandSender, Array<out Any>) -> Unit {
|
||||
return block
|
||||
}
|
@ -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<String>.matchesBeginning(list: List<Any>): 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<Command> = mutableListOf()
|
||||
|
||||
/**
|
||||
* 全部注册的指令
|
||||
* /mute -> MuteCommand
|
||||
* /jinyan -> MuteCommand
|
||||
*/
|
||||
@JvmField
|
||||
internal val requiredPrefixCommandMap: MutableMap<String, Command> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Command name of commands that are prefix optional
|
||||
* mute -> MuteCommand
|
||||
*/
|
||||
@JvmField
|
||||
internal val optionalPrefixCommandMap: MutableMap<String, Command> = 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<MessageEvent> by lazy {
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
subscribeAlways<MessageEvent>(
|
||||
concurrency = Listener.ConcurrencyKind.CONCURRENT,
|
||||
priority = Listener.EventPriority.HIGH
|
||||
) {
|
||||
if (this.sender.asCommandSender().executeCommand(message) != null) {
|
||||
intercept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal infix fun Array<out String>.intersectsIgnoringCase(other: Array<out String>): 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 <T : Any> Collection<T>.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 <T : Any> Collection<T>.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 <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = 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<out Any>,
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<Any>
|
||||
) : 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
|
||||
}
|
||||
}
|
||||
*/
|
@ -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
|
@ -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 : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
|
||||
get() = this.loader as PluginLoader<P, PluginDescription>
|
||||
|
||||
/**
|
||||
* 支持文件系统存储的扩展.
|
||||
*
|
||||
* @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` ?
|
||||
}
|
@ -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<P : Plugin, D : PluginDescription> {
|
||||
/**
|
||||
* 扫描并返回可以被加载的插件的 [描述][PluginDescription] 列表. 此函数只会被调用一次
|
||||
*/
|
||||
public fun listPlugins(): List<D>
|
||||
|
||||
/**
|
||||
* 获取此插件的描述
|
||||
*
|
||||
* @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 <D : PluginDescription, P : Plugin> PluginLoader<in P, out D>.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<P : Plugin, D : PluginDescription> : PluginLoader<P, D> {
|
||||
/**
|
||||
* 所支持的插件文件后缀, 含 '.'. 如 [JarPluginLoader] 为 ".jar"
|
||||
*/
|
||||
public val fileSuffix: String
|
||||
}
|
||||
|
||||
/**
|
||||
* [FilePluginLoader] 的默认基础实现
|
||||
*/
|
||||
public abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription>(
|
||||
public override val fileSuffix: String
|
||||
) : FilePluginLoader<P, D> {
|
||||
private fun pluginsFilesSequence(): Sequence<File> =
|
||||
PluginManager.pluginsDir.walk().filter { it.isFile && it.name.endsWith(fileSuffix, ignoreCase = true) }
|
||||
|
||||
/**
|
||||
* 读取扫描到的后缀与 [fileSuffix] 相同的文件中的 [PluginDescription]
|
||||
*/
|
||||
protected abstract fun Sequence<File>.mapToDescription(): List<D>
|
||||
|
||||
public final override fun listPlugins(): List<D> = pluginsFilesSequence().mapToDescription()
|
||||
}
|
||||
|
||||
|
||||
// Not yet decided to make public API
|
||||
internal class DeferredPluginLoader<P : Plugin, D : PluginDescription>(
|
||||
initializer: () -> PluginLoader<P, D>
|
||||
) : PluginLoader<P, D> {
|
||||
private val instance by lazy(initializer)
|
||||
|
||||
override fun listPlugins(): List<D> = 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)
|
||||
}
|
@ -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<Plugin>
|
||||
|
||||
/**
|
||||
* 内建的插件加载器列表. 由 [MiraiConsole] 初始化.
|
||||
*
|
||||
* @return 不可变的 list.
|
||||
*/
|
||||
public val builtInLoaders: List<PluginLoader<*, *>>
|
||||
|
||||
/**
|
||||
* 由插件创建的 [PluginLoader]
|
||||
*/
|
||||
public val pluginLoaders: List<PluginLoader<*, *>>
|
||||
|
||||
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)
|
||||
}
|
@ -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<PluginCenter.PluginInsight>? = null
|
||||
|
||||
/**
|
||||
* 一页 10 个 pageMinNum=1
|
||||
*/
|
||||
override suspend fun fetchPlugin(page: Int): Map<String, PluginCenter.PluginInsight> {
|
||||
check(page > 0)
|
||||
val startIndex = (page - 1) * 10
|
||||
val endIndex = startIndex + 9
|
||||
val map = mutableMapOf<String, PluginCenter.PluginInsight>()
|
||||
(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<String>("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<PluginCenter.PluginInsight>
|
||||
)
|
||||
|
||||
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 : Any> T.downloadPlugin(name: String, progressListener: T.(Float) -> Unit): File {
|
||||
TODO()
|
||||
/*
|
||||
val info = findPlugin(name) ?: error("Plugin Not Found")
|
||||
val targetFile = File(PluginManager.pluginsPath, "$name-" + info.version + ".jar")
|
||||
withContext(Dispatchers.IO) {
|
||||
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() = "崔云"
|
||||
}
|
||||
|
@ -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<String>,
|
||||
val commands: List<String>
|
||||
)
|
||||
|
||||
@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<String>,
|
||||
val author: String,
|
||||
val contact: String,
|
||||
val description: String,
|
||||
val usage: String,
|
||||
val vcs: String,
|
||||
val commands: List<String>,
|
||||
val changeLog: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取一些中心的插件基本信息,
|
||||
* 能获取到多少由实际的 [PluginCenter] 决定
|
||||
* 返回 插件名->Insight
|
||||
*/
|
||||
public suspend fun fetchPlugin(page: Int): Map<String, PluginInsight>
|
||||
|
||||
/**
|
||||
* 尝试获取到某个插件 by 全名, case sensitive
|
||||
* null 则没有
|
||||
*/
|
||||
public suspend fun findPlugin(name: String): PluginInfo?
|
||||
|
||||
|
||||
public suspend fun <T : Any> T.downloadPlugin(name: String, progressListener: T.(Float) -> Unit): File
|
||||
|
||||
public suspend fun downloadPlugin(name: String, progressListener: PluginCenter.(Float) -> Unit): File =
|
||||
downloadPlugin<PluginCenter>(name, progressListener)
|
||||
|
||||
/**
|
||||
* 刷新
|
||||
*/
|
||||
public suspend fun refresh()
|
||||
|
||||
public val name: String
|
||||
}
|
@ -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<PluginKind> 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<PluginDependency> 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
|
||||
}
|
@ -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<JvmPlugin, JvmPluginDescription>(".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<File>.mapToDescription(): List<JvmPluginDescription> {
|
||||
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<JvmPluginDescription, JvmPlugin> {
|
||||
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<JvmPlugin, JvmPlugin> {
|
||||
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()
|
||||
}
|
||||
}
|
@ -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> 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
|
||||
}
|
||||
}
|
@ -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<String, PluginClassLoader>()
|
||||
private val classesCache = mutableMapOf<String, Class<*>>()
|
||||
private val logger = SimpleLogger(loggerName) { p, message, e ->
|
||||
MiraiConsole.logger(p, "[${loggerName}]", 0, message)
|
||||
MiraiConsole.logger(p, "[${loggerName}]", 0, e)
|
||||
}
|
||||
private val logger = MiraiConsole.newLogger(loggerName)
|
||||
|
||||
/**
|
||||
* 清除所有插件加载器
|
||||
@ -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<String, Class<*>?>()
|
||||
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<String, Class<*>>()
|
||||
|
||||
internal val classesCache: Map<String, Class<*>>
|
||||
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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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, JvmPluginDescription> {
|
||||
/**
|
||||
* [JvmPlugin.loadSetting] 默认使用的实例
|
||||
*/
|
||||
@ConsoleExperimentalAPI
|
||||
public val settingStorage: SettingStorage
|
||||
|
||||
public companion object INSTANCE : JarPluginLoader by JarPluginLoaderImpl
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 <T : Setting> loadSetting(clazz: Class<T>): 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 <T : Setting> JvmPlugin.loadSetting(clazz: KClass<T>): T = this.loadSetting(clazz.java)
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun <reified T : Setting> JvmPlugin.loadSetting(): T = this.loadSetting(T::class)
|
@ -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<PluginDependency>,
|
||||
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)"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
*/
|
@ -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<PluginLoader<*, *>> = mutableListOf()
|
||||
private val loadersLock: ReentrantLock = ReentrantLock()
|
||||
private val logger = MiraiConsole.newLogger("PluginManager")
|
||||
|
||||
@JvmField
|
||||
internal val resolvedPlugins: MutableList<Plugin> = mutableListOf()
|
||||
override val plugins: List<Plugin>
|
||||
get() = resolvedPlugins.toList()
|
||||
override val builtInLoaders: List<PluginLoader<*, *>>
|
||||
get() = MiraiConsole.builtInPluginLoaders
|
||||
override val pluginLoaders: List<PluginLoader<*, *>>
|
||||
get() = _pluginLoaders.toList()
|
||||
|
||||
override val Plugin.description: PluginDescription
|
||||
get() = resolvedPlugins.firstOrNull { it == this }
|
||||
?.loader?.cast<PluginLoader<Plugin, PluginDescription>>()
|
||||
?.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 <P : Plugin, D : PluginDescription> PluginLoader<P, D>.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 <P : Plugin, D : PluginDescription> PluginLoader<P, D>.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<PluginDescriptionWithLoader>.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<PluginDescriptionWithLoader> {
|
||||
val allDescriptions =
|
||||
this.builtInLoaders.listAllPlugins()
|
||||
.asSequence()
|
||||
.onEach { (loader, descriptions) ->
|
||||
loader as PluginLoader<Plugin, PluginDescription>
|
||||
|
||||
descriptions.filter { it.kind == PluginKind.LOADER }.sortByDependencies().loadAndEnableAllInOrder()
|
||||
}
|
||||
.flatMap { it.second.asSequence() }
|
||||
|
||||
return allDescriptions.toList()
|
||||
}
|
||||
|
||||
private fun List<PluginLoader<*, *>>.listAllPlugins(): List<Pair<PluginLoader<*, *>, List<PluginDescriptionWithLoader>>> {
|
||||
return associateWith { loader -> loader.listPlugins().map { desc -> desc.wrapWith(loader) } }.toList()
|
||||
}
|
||||
|
||||
@Throws(PluginMissingDependencyException::class)
|
||||
private fun <D : PluginDescription> List<D>.sortByDependencies(): List<D> {
|
||||
val resolved = ArrayList<D>(this.size)
|
||||
|
||||
fun D.canBeLoad(): Boolean = this.dependencies.all { it.isOptional || it in resolved }
|
||||
|
||||
fun List<D>.consumeLoadable(): List<D> {
|
||||
val (canBeLoad, cannotBeLoad) = this.partition { it.canBeLoad() }
|
||||
resolved.addAll(canBeLoad)
|
||||
return cannotBeLoad
|
||||
}
|
||||
|
||||
fun List<PluginDependency>.filterIsMissing(): List<PluginDependency> =
|
||||
this.filterNot { it.isOptional || it in resolved }
|
||||
|
||||
tailrec fun List<D>.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 <D : PluginDescription> 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<PluginDescription>.contains(dependency: PluginDependency): Boolean =
|
||||
any { it.name == dependency.name }
|
@ -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<String, String> 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 <T> SerializerAwareValue<T>.provideDelegate(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T> {
|
||||
val name = property.serialName
|
||||
valueNodes.add(Node(name, this, this.serializer))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 值更新序列化器. 仅供内部使用
|
||||
*/
|
||||
@ConsoleInternalAPI
|
||||
public final override val updaterSerializer: KSerializer<Unit>
|
||||
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<String, String> by value("a" to "b")
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see JvmPlugin.loadSetting 通过 [JvmPlugin] 获取指定 [Setting] 实例.
|
||||
*/
|
||||
public interface Setting : ExperimentalSettingExtensions {
|
||||
/**
|
||||
* 使用 `by` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪.
|
||||
*/
|
||||
public operator fun <T> SerializerAwareValue<T>.provideDelegate(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T>
|
||||
|
||||
/**
|
||||
* 值更新序列化器. 仅供内部使用
|
||||
*/
|
||||
public val updaterSerializer: KSerializer<Unit>
|
||||
|
||||
/**
|
||||
* 当所属于这个 [Setting] 的 [Value] 的 [值][Value.value] 被修改时被调用.
|
||||
*/
|
||||
public fun onValueChanged(value: Value<*>)
|
||||
|
||||
/**
|
||||
* 当这个 [Setting] 被放入一个 [SettingStorage] 时调用
|
||||
*/
|
||||
public fun setStorage(storage: SettingStorage)
|
||||
}
|
||||
|
||||
@ConsoleExperimentalAPI("")
|
||||
public interface ExperimentalSettingExtensions {
|
||||
public fun <E, V, K> MutableMap<E, V>.shadowMap(
|
||||
eToK: (E) -> K,
|
||||
kToE: (K) -> E
|
||||
): MutableMap<K, V> {
|
||||
return this.shadowMap(
|
||||
kTransform = eToK,
|
||||
kTransformBack = kToE,
|
||||
vTransform = { it },
|
||||
vTransformBack = { it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//// region Setting_value_primitives CODEGEN ////
|
||||
|
||||
public fun Setting.value(default: Byte): SerializerAwareValue<Byte> = valueImpl(default)
|
||||
public fun Setting.value(default: Short): SerializerAwareValue<Short> = valueImpl(default)
|
||||
public fun Setting.value(default: Int): SerializerAwareValue<Int> = valueImpl(default)
|
||||
public fun Setting.value(default: Long): SerializerAwareValue<Long> = valueImpl(default)
|
||||
public fun Setting.value(default: Float): SerializerAwareValue<Float> = valueImpl(default)
|
||||
public fun Setting.value(default: Double): SerializerAwareValue<Double> = valueImpl(default)
|
||||
public fun Setting.value(default: Char): SerializerAwareValue<Char> = valueImpl(default)
|
||||
public fun Setting.value(default: Boolean): SerializerAwareValue<Boolean> = valueImpl(default)
|
||||
public fun Setting.value(default: String): SerializerAwareValue<String> = 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 <reified T> Setting.value(default: T): SerializerAwareValue<T> = valueFromKType(typeOf0<T>(), 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 <reified T> Setting.value(): SerializerAwareValue<T> = value(T::class.createInstance() as T)
|
||||
|
||||
/**
|
||||
* Creates a [Value] with specified [KType], and set default value.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ConsoleExperimentalAPI
|
||||
public fun <T> Setting.valueFromKType(type: KType, default: T): SerializerAwareValue<T> =
|
||||
(valueFromKTypeImpl(type) as SerializerAwareValue<Any?>).apply { this.value = default } as SerializerAwareValue<T>
|
||||
|
||||
// TODO: 2020/6/24 Introduce class TypeToken for compound types for Java.
|
@ -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 <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T
|
||||
|
||||
/**
|
||||
* 保存一个实例
|
||||
*/
|
||||
public fun store(holder: SettingHolder, setting: Setting)
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* 读取一个实例. 在 [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage]
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun <T : Setting> SettingStorage.load(holder: SettingHolder, settingClass: KClass<T>): T =
|
||||
this.load(holder, settingClass.java)
|
||||
|
||||
/**
|
||||
* 读取一个实例. 在 [T] 实例创建后 [设置 [SettingStorage]][Setting.setStorage]
|
||||
*/
|
||||
@JvmSynthetic
|
||||
public inline fun <reified T : Setting> SettingStorage.load(holder: SettingHolder): T =
|
||||
this.load(holder, T::class)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在内存存储所有 [Setting] 实例的 [SettingStorage]. 在内存数据丢失后相关 [Setting] 实例也会丢失.
|
||||
*/
|
||||
public interface MemorySettingStorage : SettingStorage, Map<Class<out Setting>, 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 <T : Setting> newSettingInstance(type: KType): T =
|
||||
newSettingInstanceUsingReflection<Setting>(type) as T
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* 创建一个 [Setting] 实例.
|
||||
*
|
||||
* @see SettingHolder.newSettingInstance
|
||||
*/
|
||||
@JvmSynthetic
|
||||
public inline fun <reified T : Setting> SettingHolder.newSettingInstance(): T {
|
||||
return this.newSettingInstance(typeOf0<T>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可以持有相关 [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 <T : Setting> newSettingInstance(type: KType): T {
|
||||
val classifier = type.classifier?.cast<KClass<*>>()?.java
|
||||
require(classifier == Setting::class.java) {
|
||||
"Cannot create Setting instance. AutoSaveSettingHolder supports only Setting type."
|
||||
}
|
||||
return AutoSaveSetting(this) as T // T is always Setting
|
||||
}
|
||||
}
|
@ -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<T> {
|
||||
public var value: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Typically returned by [Setting.value] functions.
|
||||
*/
|
||||
public class SerializableValue<T>(
|
||||
private val delegate: Value<T>,
|
||||
/**
|
||||
* The serializer used to update and dump [delegate]
|
||||
*/
|
||||
public override val serializer: KSerializer<Unit>
|
||||
) : Value<T> by delegate, SerializerAwareValue<T> {
|
||||
public override fun toString(): String = delegate.toString()
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("create")
|
||||
public fun <T> Value<T>.serializableValueWith(
|
||||
serializer: KSerializer<T>
|
||||
): SerializableValue<T> {
|
||||
return SerializableValue(
|
||||
this,
|
||||
serializer.map(serializer = { this.value }, deserializer = { this.setValueBySerializer(it) })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SerializableValue
|
||||
*/
|
||||
public interface SerializerAwareValue<T> : Value<T> {
|
||||
public val serializer: KSerializer<Unit>
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
|
||||
public fun <T> SerializerAwareValue<T>.serialize(format: StringFormat): String {
|
||||
return format.stringify(this.serializer, Unit)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
|
||||
public fun <T> SerializerAwareValue<T>.serialize(format: BinaryFormat): ByteArray {
|
||||
return format.dump(this.serializer, Unit)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
|
||||
public fun <T> SerializerAwareValue<T>.deserialize(format: StringFormat, value: String) {
|
||||
format.parse(this.serializer, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ConsoleExperimentalAPI("will be changed due to reconstruction of kotlinx.serialization")
|
||||
public fun <T> SerializerAwareValue<T>.deserialize(format: BinaryFormat, value: ByteArray) {
|
||||
format.load(this.serializer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmSynthetic
|
||||
public inline operator fun <T> Value<T>.getValue(mySetting: Any?, property: KProperty<*>): T = value
|
||||
|
||||
@JvmSynthetic
|
||||
public inline operator fun <T> Value<T>.setValue(mySetting: Any?, property: KProperty<*>, value: T) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* The serializer for a specific kind of [Value].
|
||||
*/
|
||||
public typealias ValueSerializer<T> = KSerializer<Value<T>>
|
||||
|
||||
/**
|
||||
* 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<T> : Value<T>
|
||||
|
||||
|
||||
//// region PrimitiveValues CODEGEN ////
|
||||
|
||||
/**
|
||||
* Represents a non-null [Byte] value.
|
||||
*/
|
||||
public interface ByteValue : PrimitiveValue<Byte>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Short] value.
|
||||
*/
|
||||
public interface ShortValue : PrimitiveValue<Short>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Int] value.
|
||||
*/
|
||||
public interface IntValue : PrimitiveValue<Int>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Long] value.
|
||||
*/
|
||||
public interface LongValue : PrimitiveValue<Long>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Float] value.
|
||||
*/
|
||||
public interface FloatValue : PrimitiveValue<Float>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Double] value.
|
||||
*/
|
||||
public interface DoubleValue : PrimitiveValue<Double>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Char] value.
|
||||
*/
|
||||
public interface CharValue : PrimitiveValue<Char>
|
||||
|
||||
/**
|
||||
* Represents a non-null [Boolean] value.
|
||||
*/
|
||||
public interface BooleanValue : PrimitiveValue<Boolean>
|
||||
|
||||
/**
|
||||
* Represents a non-null [String] value.
|
||||
*/
|
||||
public interface StringValue : PrimitiveValue<String>
|
||||
|
||||
//// endregion PrimitiveValues CODEGEN ////
|
||||
|
||||
|
||||
@ConsoleExperimentalAPI
|
||||
public interface CompositeValue<T> : Value<T>
|
||||
|
||||
|
||||
/**
|
||||
* Superclass of [CompositeListValue], [PrimitiveListValue].
|
||||
*/
|
||||
public interface ListValue<E> : CompositeValue<List<E>>
|
||||
|
||||
/**
|
||||
* Elements can by anything, wrapped as [Value].
|
||||
* @param E is not primitive types.
|
||||
*/
|
||||
public interface CompositeListValue<E> : ListValue<E>
|
||||
|
||||
/**
|
||||
* Elements can only be primitives, not wrapped.
|
||||
* @param E is not primitive types.
|
||||
*/
|
||||
public interface PrimitiveListValue<E> : ListValue<E>
|
||||
|
||||
|
||||
//// region PrimitiveListValue CODEGEN ////
|
||||
|
||||
public interface PrimitiveIntListValue : PrimitiveListValue<Int>
|
||||
public interface PrimitiveLongListValue : PrimitiveListValue<Long>
|
||||
// TODO + codegen
|
||||
|
||||
//// endregion PrimitiveListValue CODEGEN ////
|
||||
|
||||
|
||||
/**
|
||||
* Superclass of [CompositeSetValue], [PrimitiveSetValue].
|
||||
*/
|
||||
public interface SetValue<E> : CompositeValue<Set<E>>
|
||||
|
||||
/**
|
||||
* Elements can by anything, wrapped as [Value].
|
||||
* @param E is not primitive types.
|
||||
*/
|
||||
public interface CompositeSetValue<E> : SetValue<E>
|
||||
|
||||
/**
|
||||
* Elements can only be primitives, not wrapped.
|
||||
* @param E is not primitive types.
|
||||
*/
|
||||
public interface PrimitiveSetValue<E> : SetValue<E>
|
||||
|
||||
|
||||
//// region PrimitiveSetValue CODEGEN ////
|
||||
|
||||
public interface PrimitiveIntSetValue : PrimitiveSetValue<Int>
|
||||
public interface PrimitiveLongSetValue : PrimitiveSetValue<Long>
|
||||
// TODO + codegen
|
||||
|
||||
//// endregion PrimitiveSetValue CODEGEN ////
|
||||
|
||||
|
||||
/**
|
||||
* Superclass of [CompositeMapValue], [PrimitiveMapValue].
|
||||
*/
|
||||
public interface MapValue<K, V> : CompositeValue<Map<K, V>>
|
||||
|
||||
public interface CompositeMapValue<K, V> : MapValue<K, V>
|
||||
|
||||
public interface PrimitiveMapValue<K, V> : MapValue<K, V>
|
||||
|
||||
|
||||
//// region PrimitiveMapValue CODEGEN ////
|
||||
|
||||
public interface PrimitiveIntIntMapValue : PrimitiveMapValue<Int, Int>
|
||||
public interface PrimitiveIntLongMapValue : PrimitiveMapValue<Int, Long>
|
||||
// TODO + codegen
|
||||
|
||||
//// endregion PrimitiveSetValue CODEGEN ////
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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 <T> Setting.createCompositeSetValueImpl(tToValue: (T) -> Value<T>): CompositeSetValueImpl<T> {
|
||||
return object : CompositeSetValueImpl<T>(tToValue) {
|
||||
override fun onChanged() {
|
||||
this@createCompositeSetValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class CompositeSetValueImpl<T>(
|
||||
tToValue: (T) -> Value<T> // should override onChanged
|
||||
) : CompositeSetValue<T>, AbstractValueImpl<Set<T>>() {
|
||||
private val internalSet: MutableSet<Value<T>> = mutableSetOf()
|
||||
|
||||
private var _value: Set<T> = internalSet.shadowMap({ it.value }, tToValue).observable { onChanged() }
|
||||
|
||||
override var value: Set<T>
|
||||
get() = _value
|
||||
set(v) {
|
||||
if (_value != v) {
|
||||
@Suppress("LocalVariableName")
|
||||
val _value = _value as MutableSet<T>
|
||||
_value.clear()
|
||||
_value.addAll(v)
|
||||
onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValueBySerializer(value: Set<T>) {
|
||||
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 <T> Setting.createCompositeListValueImpl(tToValue: (T) -> Value<T>): CompositeListValueImpl<T> {
|
||||
return object : CompositeListValueImpl<T>(tToValue) {
|
||||
override fun onChanged() {
|
||||
this@createCompositeListValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class CompositeListValueImpl<T>(
|
||||
tToValue: (T) -> Value<T> // should override onChanged
|
||||
) : CompositeListValue<T>, AbstractValueImpl<List<T>>() {
|
||||
private val internalList: MutableList<Value<T>> = mutableListOf()
|
||||
|
||||
private val _value: List<T> = internalList.shadowMap({ it.value }, tToValue).observable { onChanged() }
|
||||
|
||||
override var value: List<T>
|
||||
get() = _value
|
||||
set(v) {
|
||||
if (_value != v) {
|
||||
@Suppress("LocalVariableName")
|
||||
val _value = _value as MutableList<T>
|
||||
_value.clear()
|
||||
_value.addAll(v)
|
||||
onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValueBySerializer(value: List<T>) {
|
||||
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 <K, V> Setting.createCompositeMapValueImpl(
|
||||
kToValue: (K) -> Value<K>,
|
||||
vToValue: (V) -> Value<V>
|
||||
): CompositeMapValueImpl<K, V> {
|
||||
return object : CompositeMapValueImpl<K, V>(kToValue, vToValue) {
|
||||
override fun onChanged() = this@createCompositeMapValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 2020/6/24 在一个 Value 被删除后停止追踪其更新.
|
||||
|
||||
internal abstract class CompositeMapValueImpl<K, V>(
|
||||
kToValue: (K) -> Value<K>, // should override onChanged
|
||||
vToValue: (V) -> Value<V> // should override onChanged
|
||||
) : CompositeMapValue<K, V>, AbstractValueImpl<Map<K, V>>() {
|
||||
private val internalList: MutableMap<Value<K>, Value<V>> = mutableMapOf()
|
||||
|
||||
private var _value: MutableMap<K, V> =
|
||||
internalList.shadowMap({ it.value }, kToValue, { it.value }, vToValue).observable { onChanged() }
|
||||
override var value: Map<K, V>
|
||||
get() = _value
|
||||
set(v) {
|
||||
if (_value != v) {
|
||||
@Suppress("LocalVariableName")
|
||||
val _value = _value
|
||||
_value.clear()
|
||||
_value.putAll(v)
|
||||
onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValueBySerializer(value: Map<K, V>) {
|
||||
val thisValue = this.value as MutableMap<K, V>
|
||||
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 <K, V> MutableMap<K, V>.patchImpl(_new: Map<K, V>) {
|
||||
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 <C : MutableCollection<E>, E> C.patchImpl(_new: Collection<E>) {
|
||||
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<Any?>).patchImpl(any as Collection<Any?>)
|
||||
true
|
||||
}
|
||||
this is MutableMap<*, *> && any is Map<*, *> -> {
|
||||
(this as MutableMap<Any?, Any?>).patchImpl(any as Map<Any?, Any?>)
|
||||
true
|
||||
}
|
||||
this is Value<*> && any is Value<*> -> any.value?.let { otherValue -> this.value?.tryPatch(otherValue) } == true
|
||||
else -> false
|
||||
}
|
@ -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 <reified T> typeOf0(): KType = kotlin.reflect.typeOf<T>()
|
||||
|
||||
@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<V> 的 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<Any?, Any?>(
|
||||
kToValue = { k -> valueFromKType(type.arguments[0].type!!, k) },
|
||||
vToValue = { v -> valueFromKType(type.arguments[1].type!!, v) }
|
||||
).serializableValueWith(serializerMirai(type) as KSerializer<Map<Any?, Any?>>) // 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<Any?> { v -> valueFromKType(type.arguments[0].type!!, v) }
|
||||
.serializableValueWith(serializerMirai(type) as KSerializer<List<Any?>>)
|
||||
}
|
||||
}
|
||||
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<Any?> { v -> valueFromKType(type.arguments[0].type!!, v) }
|
||||
.serializableValueWith(serializerMirai(type) as KSerializer<Set<Any?>>)
|
||||
}
|
||||
}
|
||||
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<Any?, Any?>()
|
||||
|
||||
MutableList::class,
|
||||
List::class,
|
||||
ArrayList::class
|
||||
-> mutableListOf<Any?>()
|
||||
|
||||
MutableSet::class,
|
||||
Set::class,
|
||||
LinkedHashSet::class,
|
||||
HashSet::class
|
||||
-> mutableSetOf<Any?>()
|
||||
|
||||
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 <R> 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<Any?> {
|
||||
fun serializerByKTypeImpl(type: KType): KSerializer<Any> {
|
||||
val rootClass = when (val t = type.classifier) {
|
||||
is KClass<*> -> t
|
||||
else -> error("Only KClass supported as classifier, got $t")
|
||||
} as KClass<Any>
|
||||
|
||||
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<Any, Any?>(
|
||||
typeArguments[0].classifier as KClass<Any>,
|
||||
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()
|
||||
}
|
@ -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<SerialName>()?.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<T>(
|
||||
val serialName: String,
|
||||
val value: Value<T>,
|
||||
val updaterSerializer: KSerializer<Unit>
|
||||
)
|
||||
|
||||
internal fun <T> SerializerAwareValue<T>.provideDelegateImpl(
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T> {
|
||||
val name = property.serialName
|
||||
valueNodes.add(Node(name, this, this.serializer))
|
||||
return this
|
||||
}
|
||||
|
||||
internal val valueNodes: MutableList<Node<*>> = mutableListOf()
|
||||
|
||||
internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> {
|
||||
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<Any?>
|
||||
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
|
||||
}
|
||||
}
|
@ -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<Class<out Setting>, 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 <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): 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 <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): 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 <T : Any> KClass<T>.createInstanceOrNull(): T? {
|
||||
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
|
||||
?: return null
|
||||
|
||||
return noArgsConstructor.callBy(emptyMap())
|
||||
}
|
||||
|
||||
@JvmSynthetic
|
||||
internal fun KClass<*>.findASerialName(): String =
|
||||
findAnnotation<SerialName>()?.value
|
||||
?: qualifiedName
|
||||
?: throw IllegalArgumentException("Cannot find a serial name for $this")
|
@ -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<T> : Value<T> {
|
||||
open fun setValueBySerializer(value: T) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> Value<T>.setValueBySerializer(value: T) = (this as AbstractValueImpl<T>).setValueBySerializer(value)
|
||||
|
||||
//// region PrimitiveValuesImpl CODEGEN ////
|
||||
|
||||
internal abstract class ByteValueImpl : ByteValue, SerializerAwareValue<Byte>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Byte> {
|
||||
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<Unit> 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<Short>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Short> {
|
||||
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<Unit> 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<Int>, KSerializer<Unit>, AbstractValueImpl<Int> {
|
||||
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<Unit> 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<Long>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Long> {
|
||||
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<Unit> 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<Float>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Float> {
|
||||
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<Unit> 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<Double>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Double> {
|
||||
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<Unit> 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<Char>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Char> {
|
||||
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<Unit> 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<Boolean>, KSerializer<Unit>,
|
||||
AbstractValueImpl<Boolean> {
|
||||
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<Unit> 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<String>, KSerializer<Unit>,
|
||||
AbstractValueImpl<String> {
|
||||
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<Unit> 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 ////
|
@ -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 <T : Any> Setting.valueImplPrimitive(kClass: KClass<T>): SerializerAwareValue<T>? {
|
||||
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<T>?
|
||||
}
|
||||
|
||||
|
||||
//// region Setting_value_PrimitivesImpl CODEGEN ////
|
||||
|
||||
internal fun Setting.valueImpl(default: Byte): SerializerAwareValue<Byte> {
|
||||
return object : ByteValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.byteValueImpl(): SerializerAwareValue<Byte> {
|
||||
return object : ByteValueImpl() {
|
||||
override fun onChanged() = this@byteValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Short): SerializerAwareValue<Short> {
|
||||
return object : ShortValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.shortValueImpl(): SerializerAwareValue<Short> {
|
||||
return object : ShortValueImpl() {
|
||||
override fun onChanged() = this@shortValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Int): SerializerAwareValue<Int> {
|
||||
return object : IntValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.intValueImpl(): SerializerAwareValue<Int> {
|
||||
return object : IntValueImpl() {
|
||||
override fun onChanged() = this@intValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Long): SerializerAwareValue<Long> {
|
||||
return object : LongValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.longValueImpl(): SerializerAwareValue<Long> {
|
||||
return object : LongValueImpl() {
|
||||
override fun onChanged() = this@longValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Float): SerializerAwareValue<Float> {
|
||||
return object : FloatValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.floatValueImpl(): SerializerAwareValue<Float> {
|
||||
return object : FloatValueImpl() {
|
||||
override fun onChanged() = this@floatValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Double): SerializerAwareValue<Double> {
|
||||
return object : DoubleValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.doubleValueImpl(): SerializerAwareValue<Double> {
|
||||
return object : DoubleValueImpl() {
|
||||
override fun onChanged() = this@doubleValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Char): SerializerAwareValue<Char> {
|
||||
return object : CharValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.charValueImpl(): SerializerAwareValue<Char> {
|
||||
return object : CharValueImpl() {
|
||||
override fun onChanged() = this@charValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: Boolean): SerializerAwareValue<Boolean> {
|
||||
return object : BooleanValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.booleanValueImpl(): SerializerAwareValue<Boolean> {
|
||||
return object : BooleanValueImpl() {
|
||||
override fun onChanged() = this@booleanValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.valueImpl(default: String): SerializerAwareValue<String> {
|
||||
return object : StringValueImpl(default) {
|
||||
override fun onChanged() = this@valueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
internal fun Setting.stringValueImpl(): SerializerAwareValue<String> {
|
||||
return object : StringValueImpl() {
|
||||
override fun onChanged() = this@stringValueImpl.onValueChanged(this)
|
||||
}
|
||||
}
|
||||
|
||||
//// endregion Setting_value_PrimitivesImpl CODEGEN ////
|
@ -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 <reified T : Any> KType.asKClass(): KClass<out T> {
|
||||
val clazz = requireNotNull(classifier as? KClass<T>) { "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 <reified T : Setting> newSettingInstanceUsingReflection(type: KType): T {
|
||||
val classifier = type.asKClass<T>()
|
||||
|
||||
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."
|
||||
)
|
||||
}
|
||||
}
|
@ -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<Int> 后进行 containsKey 比较的方法
|
||||
|
||||
internal inline fun <K, V, KR, VR> MutableMap<K, V>.shadowMap(
|
||||
crossinline kTransform: (K) -> KR,
|
||||
crossinline kTransformBack: (KR) -> K,
|
||||
crossinline vTransform: (V) -> VR,
|
||||
crossinline vTransformBack: (VR) -> V
|
||||
): MutableMap<KR, VR> {
|
||||
return object : MutableMap<KR, VR> {
|
||||
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<MutableMap.MutableEntry<KR, VR>>
|
||||
get() = this@shadowMap.entries.shadowMap(
|
||||
transform = { entry: MutableMap.MutableEntry<K, V> ->
|
||||
object : MutableMap.MutableEntry<KR, VR> {
|
||||
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<K, V>) -> MutableMap.MutableEntry<KR, VR>), // type inference bug
|
||||
transformBack = { entry ->
|
||||
object : MutableMap.MutableEntry<K, V> {
|
||||
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<KR>
|
||||
get() = this@shadowMap.keys.shadowMap(kTransform, kTransformBack)
|
||||
override val values: MutableCollection<VR>
|
||||
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<out KR, VR>) {
|
||||
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 <E, R> MutableCollection<E>.shadowMap(
|
||||
crossinline transform: (E) -> R,
|
||||
crossinline transformBack: (R) -> E
|
||||
): MutableCollection<R> {
|
||||
return object : MutableCollection<R> {
|
||||
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<R>): Boolean = elements.all(::contains)
|
||||
override fun isEmpty(): Boolean = this@shadowMap.isEmpty()
|
||||
override fun iterator(): MutableIterator<R> = object : MutableIterator<R> {
|
||||
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<R>): 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<R>): Boolean = elements.all(::remove)
|
||||
override fun retainAll(elements: Collection<R>): Boolean = this@shadowMap.retainAll(elements.map(transformBack))
|
||||
override fun toString(): String = this@shadowMap.toString()
|
||||
override fun hashCode(): Int = this@shadowMap.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <E, R> MutableList<E>.shadowMap(
|
||||
crossinline transform: (E) -> R,
|
||||
crossinline transformBack: (R) -> E
|
||||
): MutableList<R> {
|
||||
return object : MutableList<R> {
|
||||
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<R>): 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<R> = object : MutableIterator<R> {
|
||||
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<R>): Boolean =
|
||||
this@shadowMap.addAll(index, elements.map(transformBack))
|
||||
|
||||
override fun addAll(elements: Collection<R>): Boolean = this@shadowMap.addAll(elements.map(transformBack))
|
||||
override fun clear() = this@shadowMap.clear()
|
||||
|
||||
override fun listIterator(): MutableListIterator<R> = object : MutableListIterator<R> {
|
||||
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<R> = object : MutableListIterator<R> {
|
||||
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<R>): Boolean = elements.all(::remove)
|
||||
override fun removeAt(index: Int): R = this@shadowMap.removeAt(index).let(transform)
|
||||
override fun retainAll(elements: Collection<R>): 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<R> =
|
||||
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 <E, R> MutableSet<E>.shadowMap(
|
||||
crossinline transform: (E) -> R,
|
||||
crossinline transformBack: (R) -> E
|
||||
): MutableSet<R> {
|
||||
return object : MutableSet<R> {
|
||||
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<R>): Boolean = elements.all(::contains)
|
||||
override fun isEmpty(): Boolean = this@shadowMap.isEmpty()
|
||||
override fun iterator(): MutableIterator<R> = object : MutableIterator<R> {
|
||||
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<R>): 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<R>): Boolean = elements.all(::remove)
|
||||
override fun retainAll(elements: Collection<R>): Boolean = this@shadowMap.retainAll(elements.map(transformBack))
|
||||
override fun toString(): String = this@shadowMap.toString()
|
||||
override fun hashCode(): Int = this@shadowMap.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <T> dynamicList(crossinline supplier: () -> List<T>): List<T> {
|
||||
return object : List<T> {
|
||||
override val size: Int get() = supplier().size
|
||||
override fun contains(element: T): Boolean = supplier().contains(element)
|
||||
override fun containsAll(elements: Collection<T>): 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<T> = supplier().iterator()
|
||||
override fun lastIndexOf(element: T): Int = supplier().lastIndexOf(element)
|
||||
override fun listIterator(): ListIterator<T> = supplier().listIterator()
|
||||
override fun listIterator(index: Int): ListIterator<T> = supplier().listIterator(index)
|
||||
override fun subList(fromIndex: Int, toIndex: Int): List<T> = supplier().subList(fromIndex, toIndex)
|
||||
override fun toString(): String = supplier().toString()
|
||||
override fun hashCode(): Int = supplier().hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <T> dynamicSet(crossinline supplier: () -> Set<T>): Set<T> {
|
||||
return object : Set<T> {
|
||||
override val size: Int get() = supplier().size
|
||||
override fun contains(element: T): Boolean = supplier().contains(element)
|
||||
override fun containsAll(elements: Collection<T>): Boolean = supplier().containsAll(elements)
|
||||
override fun isEmpty(): Boolean = supplier().isEmpty()
|
||||
override fun iterator(): Iterator<T> = supplier().iterator()
|
||||
override fun toString(): String = supplier().toString()
|
||||
override fun hashCode(): Int = supplier().hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal inline fun <T> dynamicMutableList(crossinline supplier: () -> MutableList<T>): MutableList<T> {
|
||||
return object : MutableList<T> {
|
||||
override val size: Int get() = supplier().size
|
||||
override fun contains(element: T): Boolean = supplier().contains(element)
|
||||
override fun containsAll(elements: Collection<T>): 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<T> = 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<T>): Boolean = supplier().addAll(index, elements)
|
||||
override fun addAll(elements: Collection<T>): Boolean = supplier().addAll(elements)
|
||||
override fun clear() = supplier().clear()
|
||||
override fun listIterator(): MutableListIterator<T> = supplier().listIterator()
|
||||
override fun listIterator(index: Int): MutableListIterator<T> = supplier().listIterator(index)
|
||||
override fun remove(element: T): Boolean = supplier().remove(element)
|
||||
override fun removeAll(elements: Collection<T>): Boolean = supplier().removeAll(elements)
|
||||
override fun removeAt(index: Int): T = supplier().removeAt(index)
|
||||
override fun retainAll(elements: Collection<T>): Boolean = supplier().retainAll(elements)
|
||||
override fun set(index: Int, element: T): T = supplier().set(index, element)
|
||||
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> = supplier().subList(fromIndex, toIndex)
|
||||
override fun toString(): String = supplier().toString()
|
||||
override fun hashCode(): Int = supplier().hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal inline fun <T> dynamicMutableSet(crossinline supplier: () -> MutableSet<T>): MutableSet<T> {
|
||||
return object : MutableSet<T> {
|
||||
override val size: Int get() = supplier().size
|
||||
override fun contains(element: T): Boolean = supplier().contains(element)
|
||||
override fun containsAll(elements: Collection<T>): Boolean = supplier().containsAll(elements)
|
||||
override fun isEmpty(): Boolean = supplier().isEmpty()
|
||||
override fun iterator(): MutableIterator<T> = supplier().iterator()
|
||||
override fun add(element: T): Boolean = supplier().add(element)
|
||||
override fun addAll(elements: Collection<T>): Boolean = supplier().addAll(elements)
|
||||
override fun clear() = supplier().clear()
|
||||
override fun remove(element: T): Boolean = supplier().remove(element)
|
||||
override fun removeAll(elements: Collection<T>): Boolean = supplier().removeAll(elements)
|
||||
override fun retainAll(elements: Collection<T>): 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 <K, V> MutableMap<K, V>.observable(crossinline onChanged: () -> Unit): MutableMap<K, V> {
|
||||
return object : MutableMap<K, V>, Map<K, V> by (this as Map<K, V>) {
|
||||
override val keys: MutableSet<K>
|
||||
get() = this@observable.keys.observable(onChanged)
|
||||
override val values: MutableCollection<V>
|
||||
get() = this@observable.values.observable(onChanged)
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
||||
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<out K, V>) = 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 <T> MutableList<T>.observable(crossinline onChanged: () -> Unit): MutableList<T> {
|
||||
return object : MutableList<T> {
|
||||
override val size: Int get() = this@observable.size
|
||||
override fun contains(element: T): Boolean = this@observable.contains(element)
|
||||
override fun containsAll(elements: Collection<T>): 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<T> = object : MutableIterator<T> {
|
||||
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<T>): Boolean =
|
||||
this@observable.addAll(index, elements).also { onChanged() }
|
||||
|
||||
override fun addAll(elements: Collection<T>): Boolean = this@observable.addAll(elements).also { onChanged() }
|
||||
override fun clear() = this@observable.clear().also { onChanged() }
|
||||
override fun listIterator(): MutableListIterator<T> = object : MutableListIterator<T> {
|
||||
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<T> = object : MutableListIterator<T> {
|
||||
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<T>): Boolean =
|
||||
this@observable.removeAll(elements).also { onChanged() }
|
||||
|
||||
override fun removeAt(index: Int): T = this@observable.removeAt(index).also { onChanged() }
|
||||
override fun retainAll(elements: Collection<T>): 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<T> = this@observable.subList(fromIndex, toIndex)
|
||||
override fun toString(): String = this@observable.toString()
|
||||
override fun hashCode(): Int = this@observable.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <T> MutableCollection<T>.observable(crossinline onChanged: () -> Unit): MutableCollection<T> {
|
||||
return object : MutableCollection<T> {
|
||||
override val size: Int get() = this@observable.size
|
||||
override fun contains(element: T): Boolean = this@observable.contains(element)
|
||||
override fun containsAll(elements: Collection<T>): Boolean = this@observable.containsAll(elements)
|
||||
override fun isEmpty(): Boolean = this@observable.isEmpty()
|
||||
override fun iterator(): MutableIterator<T> = object : MutableIterator<T> {
|
||||
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<T>): 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<T>): Boolean =
|
||||
this@observable.removeAll(elements).also { onChanged() }
|
||||
|
||||
override fun retainAll(elements: Collection<T>): Boolean =
|
||||
this@observable.retainAll(elements).also { onChanged() }
|
||||
|
||||
override fun toString(): String = this@observable.toString()
|
||||
override fun hashCode(): Int = this@observable.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <T> MutableSet<T>.observable(crossinline onChanged: () -> Unit): MutableSet<T> {
|
||||
return object : MutableSet<T> {
|
||||
override val size: Int get() = this@observable.size
|
||||
override fun contains(element: T): Boolean = this@observable.contains(element)
|
||||
override fun containsAll(elements: Collection<T>): Boolean = this@observable.containsAll(elements)
|
||||
override fun isEmpty(): Boolean = this@observable.isEmpty()
|
||||
override fun iterator(): MutableIterator<T> = object : MutableIterator<T> {
|
||||
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<T>): 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<T>): Boolean =
|
||||
this@observable.removeAll(elements).also { onChanged() }
|
||||
|
||||
override fun retainAll(elements: Collection<T>): 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 <R : Any> Any.smartCastPrimitive(clazz: KClass<R>): R {
|
||||
kotlin.runCatching {
|
||||
return Yaml.default.parse(clazz.serializer(), this.toString())
|
||||
}.getOrElse {
|
||||
throw IllegalArgumentException("Cannot cast '$this' to ${clazz.qualifiedName}", it)
|
||||
}
|
||||
}
|
||||
|
@ -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<SerialName>()?.value ?: this.name
|
||||
|
||||
internal fun Int.isOdd() = this and 0b1 != 0
|
||||
|
||||
internal inline fun <E> KSerializer<E>.bind(
|
||||
crossinline setter: (E) -> Unit,
|
||||
crossinline getter: () -> E
|
||||
): KSerializer<E> {
|
||||
return object : KSerializer<E> {
|
||||
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 <E, R> KSerializer<E>.map(
|
||||
crossinline serializer: (R) -> E,
|
||||
crossinline deserializer: (E) -> R
|
||||
): KSerializer<R> {
|
||||
return object : KSerializer<R> {
|
||||
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))
|
||||
}
|
||||
}
|
@ -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<Long>
|
||||
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<Long, MutableSet<Long>> by value()
|
||||
|
||||
internal operator fun get(bot: Bot): MutableSet<Long> = 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 <reified T : Setting> load(): T = load(ConsoleBuiltInSettingHolder)
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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 = ""
|
||||
)
|
@ -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<Void?> {
|
||||
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<Void?> {
|
||||
return future {
|
||||
delay(delayMillis)
|
||||
withContext(Dispatchers.IO) {
|
||||
runnable.run()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增一个 Delayed Task (延迟任务)
|
||||
*
|
||||
* 在延迟 [delayMillis] 后执行 [runnable]
|
||||
*/
|
||||
public fun <R> delayed(delayMillis: Long, runnable: Callable<R>): CompletableFuture<Void?> {
|
||||
return future {
|
||||
delay(delayMillis)
|
||||
withContext(Dispatchers.IO) { runnable.call() }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行一个任务, 最终返回 [Future], 与 Java 使用方法无异, 但效率更高且可以在插件关闭时停止
|
||||
*/
|
||||
public fun <R> async(supplier: Callable<R>): Future<R> {
|
||||
return future {
|
||||
withContext(Dispatchers.IO) { supplier.call() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行一个任务, 没有返回
|
||||
*/
|
||||
public fun async(runnable: Runnable): Future<Void?> {
|
||||
return future {
|
||||
withContext(Dispatchers.IO) { runnable.run() }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<Semver> by String.serializer().map(
|
||||
serializer = { it.toString() },
|
||||
deserializer = { Semver(it, Semver.SemverType.LOOSE) }
|
||||
)
|
||||
|
||||
@Serializer(forClass = Semver::class)
|
||||
internal object SemverAsStringSerializerIvy : KSerializer<Semver> by String.serializer().map(
|
||||
serializer = { it.toString() },
|
||||
deserializer = { Semver(it, Semver.SemverType.IVY) }
|
||||
)
|
@ -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 <R> retryCatching(n: @Range(from = 1, to = Int.MAX_VALUE.toLong()) Int, block: () -> R): Result<R> {
|
||||
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!!)
|
||||
}
|
@ -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<PluginLoader<*, *>> = 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<Any?>? = null
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun <R> withTesting(timeout: Long = 5000L, block: suspend () -> Unit): R {
|
||||
@Suppress("RemoveExplicitTypeArguments") // bug
|
||||
return if (timeout != -1L) {
|
||||
withTimeout<R>(timeout) {
|
||||
suspendCancellableCoroutine<R> { ct ->
|
||||
this@Testing.cont = ct as Continuation<Any?>
|
||||
runBlocking { block() }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
suspendCancellableCoroutine<R> { ct ->
|
||||
this.cont = ct as Continuation<Any?>
|
||||
runBlocking { block() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ok(result: Any? = Unit) {
|
||||
val cont = cont
|
||||
assertNotNull(cont)
|
||||
cont.resume(result)
|
||||
}
|
||||
}
|
@ -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<out Any>) {
|
||||
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<Array<String>> {
|
||||
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<Array<String>> {
|
||||
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<Array<Any>> {
|
||||
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<Int> {
|
||||
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<IllegalStateException> {
|
||||
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<MyClass> {
|
||||
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<MyClass> { execute(sender, "mute 333") }.value)
|
||||
assertEquals(2, withTesting<MyClass> { 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") })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 : Command, R> T.withRegistration(block: T.() -> R): R {
|
||||
this.register()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
this.unregister()
|
||||
}
|
||||
}
|
@ -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<MutableMap<String, String>>()
|
||||
val map2 by value<MutableMap<String, MutableMap<String, String>>>()
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
119
build.gradle.kts
119
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<MiraiConsoleBuildPlugin>()
|
||||
|
||||
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<String, File> {
|
||||
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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
129
buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt
Normal file
129
buildSrc/src/main/kotlin/MiraiConsoleBuildPlugin.kt
Normal file
@ -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<Project> {
|
||||
override fun apply(target: Project) = target.run {
|
||||
apply<ShadowPlugin>()
|
||||
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<String, File> {
|
||||
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()
|
||||
}
|
126
buildSrc/src/main/kotlin/PublishingHelpers.kt
Normal file
126
buildSrc/src/main/kotlin/PublishingHelpers.kt
Normal file
@ -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 <U : Task> RegisteringDomainObjectDelegateProviderWithTypeAndAction<out TaskContainer, U>.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 <T> ExistingDomainObjectDelegate<out T>.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")
|
||||
|
||||
}
|
45
buildSrc/src/main/kotlin/SetCompileTargetPlugin.kt
Normal file
45
buildSrc/src/main/kotlin/SetCompileTargetPlugin.kt
Normal file
@ -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<out Any>): Pair<Any, Method> {
|
||||
return this to this::class.java.getMethod(name, *params.map { it.java }.toTypedArray())
|
||||
}
|
||||
|
||||
operator fun Pair<Any, Method>.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"
|
||||
}
|
||||
}
|
||||
}
|
28
buildSrc/src/main/kotlin/Versions.kt
Normal file
28
buildSrc/src/main/kotlin/Versions.kt
Normal file
@ -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"
|
||||
}
|
14
buildSrc/src/main/kotlin/dependencyExtensions.kt
Normal file
14
buildSrc/src/main/kotlin/dependencyExtensions.kt
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
|
1
frontend/mirai-android
Submodule
1
frontend/mirai-android
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 9784f7cd3881c64c85c2c191e40f18d9ecf9e40b
|
@ -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")
|
||||
}
|
@ -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<MiraiGraphicalUI>()
|
||||
}
|
@ -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<MiraiGraphicalUIController>(),MiraiConsoleGraphicalLoader.coreVersion,MiraiConsoleGraphicalLoader.consoleVersion)
|
||||
MiraiConsole.start(
|
||||
find<MiraiGraphicalFrontEndController>(),
|
||||
MiraiConsoleGraphicalLoader.coreVersion,
|
||||
MiraiConsoleGraphicalLoader.consoleVersion
|
||||
)
|
||||
}
|
||||
|
||||
override fun stop() {
|
@ -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<GlobalSettingModel>()
|
||||
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<PluginModel> =
|
||||
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) {
|
@ -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<Bot>(null)
|
||||
var bot: Bot by botProperty
|
||||
|
||||
val logHistory = observableListOf<Pair<String, String>>()
|
||||
val logger: SimpleLogger =
|
||||
SimpleLogger(uin.toString()) { priority: SimpleLogger.LogPriority, message: String?, e: Throwable? ->
|
||||
|
||||
val frontend = find<MiraiGraphicalFrontEndController>()
|
||||
|
||||
frontend.run {
|
||||
logHistory.apply {
|
||||
val time = sdf.format(Date())
|
||||
add("[$time] $uin $message" to priority.name)
|
||||
trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val admins = observableListOf<Long>()
|
||||
}
|
||||
|
||||
class BotViewModel(botModel: BotModel? = null) : ItemViewModel<BotModel>(botModel) {
|
||||
val bot = bind(BotModel::botProperty)
|
||||
val logHistory = bind(BotModel::logHistory)
|
||||
val admins = bind(BotModel::admins)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user