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:
Him188 2020-08-01 13:10:20 +08:00
commit c4d98f1c9a
162 changed files with 10079 additions and 4056 deletions

View File

@ -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 }}

View File

@ -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
View File

@ -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
View File

@ -0,0 +1,3 @@
[submodule "frontend/mirai-android"]
path = frontend/mirai-android
url = https://github.com/mzdluo123/MiraiAndroid

View File

@ -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;

View 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"))
}

View File

@ -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()

View File

@ -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("---------------------------------------------")
}
}
}

View File

@ -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())
}
}
})
}

View File

@ -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()

View File

@ -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"

View File

@ -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>()
"""
)
}

View File

@ -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" }
}

View 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>

View 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

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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 {
}
}
}
*/

View File

@ -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]

View File

@ -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
}

View File

@ -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')"
}

View File

@ -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
)
}
)
}

View File

@ -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)
}
}

View File

@ -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)"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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>)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}
*/

View File

@ -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

View File

@ -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` ?
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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() = "崔云"
}

View File

@ -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
}

View File

@ -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
}

View 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()
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)"
}
}

View 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)
}
*/

View File

@ -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 }

View File

@ -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.

View File

@ -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
}
}

View File

@ -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 ////

View File

@ -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
}

View File

@ -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()
}

View 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("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
}
}

View File

@ -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")

View File

@ -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 ////

View File

@ -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 ////

View File

@ -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."
)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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) }
}

View File

@ -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 = ""
)

View File

@ -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
}
}
}

View File

@ -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)
}

View 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
*/
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) }
)

View File

@ -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!!)
}

View File

@ -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)
}
}

View File

@ -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") })
}
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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")
}

View 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()
}

View 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")
}

View 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"
}
}
}

View 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"
}

View 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)
}

View File

@ -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
)
)

@ -0,0 +1 @@
Subproject commit 9784f7cd3881c64c85c2c191e40f18d9ecf9e40b

View File

@ -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")
}

View File

@ -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>()
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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