From d318f0f7681e990f205875d6e0b779b031dcb5be Mon Sep 17 00:00:00 2001 From: Madray Haven Date: Sat, 18 Nov 2023 15:24:49 +0800 Subject: [PATCH] feature: finish cqcode serialization --- build.gradle.kts | 3 +- buildSrc/build.gradle.kts | 13 ++ buildSrc/src/main/kotlin/_VersionGen.kt | 71 +++++++ gradle.properties | 2 +- gradle/mystere.versions.toml | 11 +- kotlinx-serialization-cqcode/build.gradle.kts | 4 +- .../mystere/serialization/cqcode/CQCode.kt | 129 +----------- .../serialization/cqcode/CQCodeMessage.kt | 20 +- .../cqcode/CQCodeMessageDecoder.kt | 145 ++++++++++++++ .../cqcode/CQCodeMessageEncoder.kt | 76 +++++++ .../serialization/cqcode/CQCodeMessageItem.kt | 149 +++++++++----- .../cqcode/CQCodeMessageItemDecoder.kt | 98 ++------- .../cqcode/CQCodeMessageItemEncoder.kt | 77 +------ .../mystere/serialization/cqcode/ICoder.kt | 165 +++++++++++++++ .../serialization/cqcode/CQCodeTest.kt | 28 ++- .../mystere/serialization/cqcode/Util.kt | 17 ++ mystere-core/build.gradle.kts | 6 +- .../github/mystere/core/MystereBotConfig.kt | 4 +- mystere-qq/build.gradle.kts | 35 +++- .../kotlin/io/github/mystere/qq/QQBot.kt | 11 +- .../websocket/QQBotWebsocketConnection.kt | 23 +-- .../kotlin/io/github/mystere/qq/util/_Log.kt | 26 --- .../mystere/sqlite/MystereQQDatabase.sq | 0 mystere-util/build.gradle.kts | 20 +- .../kotlin/io/github/mystere}/util/_Ktor.kt | 14 +- .../kotlin/io/github/mystere/util/_Log.kt | 23 +++ .../io/github/mystere/util/_Ktor.jvm.kt | 11 + .../io/github/mystere/util/_Ktor.linux.kt | 11 + .../io/github/mystere/util/_Ktor.darwin.kt | 12 ++ .../io/github/mystere/util/_Ktor.mingw.kt | 11 + mystere/build.gradle.kts | 18 +- .../kotlin/io/github/mystere/app/Mystere.kt | 13 +- onebot-api/build.gradle.kts | 5 +- .../io/github/mystere/onebot/OneBotConfig.kt | 20 +- .../github/mystere/onebot/OneBotException.kt | 5 + onebot-v11/build.gradle.kts | 31 ++- .../onebot/v11/IOneBotV11Connection.kt | 36 ++++ .../github/mystere/onebot/v11/OneBotAction.kt | 26 +++ .../github/mystere/onebot/v11/OneBotConfig.kt | 45 ++++- .../github/mystere/onebot/v11/OneBotEvent.kt | 188 ++++++++++++++++++ .../onebot/v11/connection/ReverseWebSocket.kt | 41 ++++ onebot-v12/build.gradle.kts | 5 +- .../github/mystere/onebot/v12/OneBotConfig.kt | 29 ++- .../github/mystere/onebot/v12/OneBotEvent.kt | 5 + 44 files changed, 1238 insertions(+), 444 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/_VersionGen.kt create mode 100644 kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageDecoder.kt create mode 100644 kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageEncoder.kt create mode 100644 kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/ICoder.kt delete mode 100644 mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Log.kt create mode 100644 mystere-qq/src/main/sqldelight/io/github/mystere/sqlite/MystereQQDatabase.sq rename {mystere-qq/src/commonMain/kotlin/io/github/mystere/qq => mystere-util/src/commonMain/kotlin/io/github/mystere}/util/_Ktor.kt (62%) create mode 100644 mystere-util/src/jvmMain/kotlin/io/github/mystere/util/_Ktor.jvm.kt create mode 100644 mystere-util/src/linuxMain/kotlin/io/github/mystere/util/_Ktor.linux.kt create mode 100644 mystere-util/src/macosMain/kotlin/io/github/mystere/util/_Ktor.darwin.kt create mode 100644 mystere-util/src/mingwMain/kotlin/io/github/mystere/util/_Ktor.mingw.kt create mode 100644 onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotException.kt create mode 100644 onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/IOneBotV11Connection.kt create mode 100644 onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotAction.kt create mode 100644 onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotEvent.kt create mode 100644 onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/connection/ReverseWebSocket.kt create mode 100644 onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotEvent.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9d05458..ee9ce99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,4 +5,5 @@ plugins { alias(mystere.plugins.ktorfit) apply false alias(mystere.plugins.ksp) apply false alias(mystere.plugins.buildkonfig) apply false -} + alias(mystere.plugins.sqldelight) apply false +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..5b015ed --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,13 @@ +repositories { + google() + mavenCentral() + gradlePluginPortal() +} + +plugins { + `kotlin-dsl` +} + +dependencies { + +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/_VersionGen.kt b/buildSrc/src/main/kotlin/_VersionGen.kt new file mode 100644 index 0000000..cd675b2 --- /dev/null +++ b/buildSrc/src/main/kotlin/_VersionGen.kt @@ -0,0 +1,71 @@ +import org.gradle.api.Project +import java.io.InputStream +import java.math.BigInteger +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.* + + +val Project.MYSTERE_LIB: String get() { + return findProperty("mystere.lib.version")!!.toString() +} +val Project.MYSTERE_APP: String get() { + return findProperty("mystere.app.version")!!.toString() +} +/** + * git commit id,若当前不为 git 仓库,则返回 TIME_MD5,可用于版本名后缀 + * @see TIME_MD5 + */ +val GIT_HEAD: String get() { + return withRuntime("git rev-parse --short HEAD") { + inputStream.useFirstLineIfNotBlack() ?: TIME_MD5 + } +} + +/** + * 按照 yyMMdd 的格式,根据当前日期返回一个整数,可用于版本号 + */ +val DATED_VERSION: Int @Suppress("SimpleDateFormat") get() { + return Integer.parseInt(SimpleDateFormat("yyMMdd").format(Date())) +} + +/** + * 按照 yyMMdd 的格式,根据最近一条 git commit 日期返回一个整数,若当前不为 git 仓库,则返回 DATED_VERSION,可用于版本号 + * @see DATED_VERSION + */ +val COMMIT_DATE_VERSION: Int get() { + return withRuntime("git log -n 1 --pretty=format:%cd --date=format:%y%m%d") { + inputStream.useFirstLineIfNotBlack()?.toInt() ?: DATED_VERSION + } +} + +/** + * 返回当前日期的 MD5,截取其中十位,与 git commit id 位数相等,可用于版本名后缀 + */ +val TIME_MD5: String get() { + val digest = MessageDigest.getInstance("MD5") + .digest(System.currentTimeMillis().toString().toByteArray()) + val pre = BigInteger(1, digest) + return pre.toString(16).padStart(32, '0').substring(8, 18) +} + + + +inline fun withRuntime(command: String, crossinline block: Process.() -> T): T { + val process = Runtime.getRuntime().exec(command) + return block.invoke(process) +} + +fun InputStream.useFirstLineIf(block: (String) -> Boolean): String? { + val firstLine = reader().readLines() + .takeIf { it.isNotEmpty() } + ?.get(0) + return firstLine?.takeIf { block.invoke(it) } +} + +fun InputStream.useFirstLineIfNotBlack(): String? { + return useFirstLineIf { + it.isNotBlank() + } +} + diff --git a/gradle.properties b/gradle.properties index d9f058c..991b1e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx4096M -Dkotlin.daemon.jvm.options\="-Xmx4096M" #Kotlin kotlin.code.style=official diff --git a/gradle/mystere.versions.toml b/gradle/mystere.versions.toml index 6ba8659..6a79148 100644 --- a/gradle/mystere.versions.toml +++ b/gradle/mystere.versions.toml @@ -5,11 +5,13 @@ ktor = "2.3.5" ktorfit = "1.9.1" ksp = "1.9.20-1.0.14" buildkonfig = "0.14.0" +sqldelight = "2.0.0" kotlinx-coroutines = "1.7.3" kotlinx-serialization-json = "1.6.0" kotlinx-serialization-message-pack = "0.0.2" kotlinx-io= "0.3.0" +kotlinx-datetime= "0.4.1" kotlin-logging = "5.1.0" slf4j = "2.0.9" @@ -29,13 +31,16 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version. kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } ktor-client-winhttp = { group = "io.ktor", name = "ktor-client-winhttp", version.ref = "ktor" } -ktor-plugin-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } +ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } +ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } +ktor-plugin-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-plugin-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } kotlinx-serialization-message-pack = { group = "com.wunderbee.kompack", name = "kompack-kotlin-serialization", version.ref = "ktor" } @@ -46,6 +51,9 @@ yamlkt = { group = "net.mamoe.yamlkt", name = "yamlkt", version.ref = "yamlkt" } ktorfit-lib-light = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-lib-light", version.ref = "ktorfit" } ktorfit-ksp = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-ksp", version.ref = "ktorfit" } +sqldelight-driver-sqlite-native = { group = "app.cash.sqldelight", name = "native-driver", version.ref = "sqldelight" } +sqldelight-driver-sqlite-jvm = { group = "app.cash.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" } + # :mystere clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" } @@ -60,6 +68,7 @@ ktor = { id = "io.ktor.plugin", version.ref = "ktor" } ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } [bundles] diff --git a/kotlinx-serialization-cqcode/build.gradle.kts b/kotlinx-serialization-cqcode/build.gradle.kts index 80f8e34..210bdb7 100644 --- a/kotlinx-serialization-cqcode/build.gradle.kts +++ b/kotlinx-serialization-cqcode/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) @@ -92,6 +94,6 @@ buildkonfig { packageName = findProperty("mystere.lib.serialization.cqcode.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) } } diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCode.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCode.kt index 2cfc0d9..8b3eb12 100644 --- a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCode.kt +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCode.kt @@ -9,6 +9,8 @@ import kotlinx.serialization.modules.SerializersModule internal val CQCodeJson = Json { ignoreUnknownKeys = true explicitNulls = true + useAlternativeNames = true + useArrayPolymorphism = false } sealed class CQCode( @@ -26,113 +28,21 @@ sealed class CQCode( } fun decodeFromJson(element: JsonArray): CQCodeMessage { - val result = ArrayDeque() - for (item in element) { - result.addLast(decodeSingleItemFromJson(item.jsonObject)) - } - return CQCodeMessage(result) + return CQCodeJson.decodeFromJsonElement(element) } fun decodeFromString(string: String): CQCodeMessage { try { if (string.startsWith("[{\"") && string.endsWith("\"}]")) { - return decodeFromJson( - CQCodeJson.decodeFromString(JsonArray.serializer(), string) - ) - } else { - val result = ArrayDeque() - - var index = 0 - val chars = string.toCharArray() - while (index < chars.size) { - when (chars[index]) { - '[' -> { - val startIndex = index - try { - while (chars[index] != ']') { - index += 1 - } - } catch (e: IndexOutOfBoundsException) { - throw SerializationException("Not a valid CQCode: $string") - } - index += 1 - result.addLast(decodeSingleItemFromString( - string.substring(startIndex, index) - )) - } - else -> { - val startIndex = index - try { - while (chars[index] != '[') { - index += 1 - } - } catch (e: IndexOutOfBoundsException) { - throw SerializationException("Not a valid CQCode: $string") - } - result.addLast(CQCodeMessageItem.Text( - string.substring(startIndex, index) - )) - index += 1 - } - } - } - return CQCodeMessage(result) - } - } catch (e: Exception) { - throw SerializationException("Failed to decode content: $string", e) - } - } - - fun decodeSingleItemFromJson(element: JsonObject): CQCodeMessageItem { - try { - val type = element["type"]?.jsonPrimitive!!.content.asCQCodeMessageItemType - return type.decodeFromJsonElement(CQCodeJson, element["data"]!!.jsonObject) - } catch (e: Exception) { - throw SerializationException("Failed to decode element: $element", e) - } - } - fun decodeSingleItemFromString(string: String): CQCodeMessageItem { - try { - if (string.startsWith("{\"") && string.endsWith("\"}")) { - return decodeSingleItemFromJson( - CQCodeJson.decodeFromString(JsonObject.serializer(), string) - ) - } else if (string.contains("\\[(.*?)]".toRegex())) { - if (!string.startsWith("[CQ:") || !string.endsWith("]")) { - throw SerializationException("Not a valid CQCode item: $string") - } - val type = string.substring( - 4, if (string.contains(",")) { - string.indexOf(",") - } else { - string.length - 4 - }) - .takeIf { it != "text" } - ?.lowercase() - ?: throw SerializationException("Not a valid CQCode: $string") - val args = with(string.substring(type.length + 5, string.length - 1)) { - HashMap().also { - it["type"] = type - for (item in split(",")) { - if (!item.contains("=")) { - continue - } - val keyValue = item.split("=") - it[keyValue[0]] = keyValue[1] - } - } - } - return type.asCQCodeMessageItemType.deserialize(CQCodeMessageItemDecoder( - string, args, serializersModule - )) + return CQCodeJson.decodeFromString(CQCodeMessage.serializer(), string) } else { - return CQCodeMessageItem.Text(string) + return CQCodeMessage.serializer() + .deserialize(CQCodeMessageDecoder(string, serializersModule)) } } catch (e: Exception) { throw SerializationException("Failed to decode content: $string", e) } } - @Deprecated( message = "Use encodeToString instead.", replaceWith = ReplaceWith("encodeToString(string)"), @@ -142,31 +52,12 @@ sealed class CQCode( return encodeToString(value as CQCodeMessage) } fun encodeToJson(value: CQCodeMessage): JsonArray { - return buildJsonArray { - for (item in value.chain) { - add(encodeSingleItemToJson(item)) - } - } + return CQCodeJson.encodeToJsonElement(value).jsonArray } fun encodeToString(value: CQCodeMessage): String { - return StringBuilder().also { - for (item in value.chain) { - it.append(encodeSingleItemToString(item)) - } - }.toString() - } - fun encodeSingleItemToJson(element: CQCodeMessageItem): JsonObject { - return CQCodeJson.encodeToJsonElement(element).jsonObject - } - fun encodeSingleItemToString(value: T): String { - when (value) { - is CQCodeMessageItem.Text -> return value.text - else -> { - val encoder = CQCodeMessageItemEncoder(serializersModule) - value.type.serialize(encoder, value) - return encoder.encodeFinalResult() - } - } + val encoder = CQCodeMessageEncoder(serializersModule) + encoder.encodeCQCodeMessage(value) + return encoder.encodeFinalResult() } companion object: CQCode( diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessage.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessage.kt index c317850..607e9df 100644 --- a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessage.kt +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessage.kt @@ -3,8 +3,8 @@ package io.github.mystere.serialization.cqcode import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* @@ -34,15 +34,12 @@ data class CQCodeMessage internal constructor( } object CQCodeMessageSerializer: KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor( - "io.github.mystere.serializer.cqcode.CQCodeMessage", - ) { - } + override val descriptor: SerialDescriptor = listSerialDescriptor() override fun deserialize(decoder: Decoder): CQCodeMessage { try { - if (decoder is JsonDecoder) { - when (val element = decoder.decodeJsonElement()) { + when (decoder) { + is JsonDecoder -> when (val element = decoder.decodeJsonElement()) { is JsonPrimitive -> { if (element.isString) { return CQCode.decodeFromString(element.content) @@ -53,6 +50,7 @@ object CQCodeMessageSerializer: KSerializer { } else -> { } } + is CQCodeMessageDecoder -> return decoder.decodeCQCodeMessage() } throw SerializationException("Unsupported decoder type: ${decoder::class}") } catch (e: Exception) { @@ -66,10 +64,14 @@ object CQCodeMessageSerializer: KSerializer { is JsonEncoder -> { encoder.encodeJsonElement(buildJsonArray { for (item in value.chain) { - add(CQCodeJson.encodeToJsonElement(item)) + add(buildJsonObject { + put("type", JsonPrimitive(item._type.name)) + put("data", item._type.encodeToJsonElement(CQCodeJson, item)) + }) } }) } + is CQCodeMessageEncoder -> encoder.encodeCQCodeMessage(value) else -> throw SerializationException("Unsupported encoder type: ${encoder::class}") } } catch (e: Exception) { diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageDecoder.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageDecoder.kt new file mode 100644 index 0000000..1f653b3 --- /dev/null +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageDecoder.kt @@ -0,0 +1,145 @@ +package io.github.mystere.serialization.cqcode + +import io.github.mystere.util.logger +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.modules.SerializersModule + +class CQCodeMessageDecoder( + private val origin: String, + override val serializersModule: SerializersModule, +) : ICQCodeDecoder { + fun decodeCQCodeMessage(): CQCodeMessage { + with(beginStructure(CQCodeMessageSerializer.descriptor)) { + val result = HashMap() + var index: Int + do { + index = decodeElementIndex(CQCodeMessageSerializer.descriptor) + if (index == CompositeDecoder.DECODE_DONE) { + continue + } + val string = decodeStringElement(CQCodeMessageSerializer.descriptor, index) + try { + if (string.startsWith("{\"") && string.endsWith("\"}")) { + val element = CQCodeJson.decodeFromString(string) + val type = io.github.mystere.serialization.cqcode.CQCodeMessageItem.Type.valueOf(element["type"]?.jsonPrimitive!!.content) + result[index] = type.decodeFromJsonElement(CQCodeJson, element["data"]!!.jsonObject) + } else if (string.contains("\\[(.*?)]".toRegex())) { + if (!string.startsWith("[CQ:") || !string.endsWith("]")) { + throw SerializationException("Not a valid CQCode item: $string") + } + val type = string.substring( + 4, if (string.contains(",")) { + string.indexOf(",") + } else { + string.length - 4 + }) + .takeIf { it != "text" } + ?.lowercase() + ?: throw SerializationException("Not a valid CQCode: $string") + result[index] = with(io.github.mystere.serialization.cqcode.CQCodeMessageItem.Type.valueOf(type)) { + deserialize( + CQCodeMessageItemDecoder( + string, this, serializersModule + ) + ) + } + } else { + result[index] = CQCodeMessageItem.Text(string) + } + } catch (e: Exception) { + throw SerializationException("Failed to decode content: $string", e) + } + } while (index != CompositeDecoder.DECODE_DONE) + return CQCodeMessage(ArrayDeque(result.size).also { + for (index in 0 until result.size) { + it.add(result[index]!!) + } + }) + } + } + + + private val items by lazy { + val result = ArrayDeque() + + var index = 0 + val chars = origin.toCharArray() + while (index < chars.size) { + when (chars[index]) { + '[' -> { + val startIndex = index + try { + while (chars[index] != ']') { + index += 1 + } + } catch (e: IndexOutOfBoundsException) { + throw SerializationException("Not a valid CQCode: $origin") + } + index += 1 + result.addLast(origin.substring(startIndex, index)) + } + else -> { + val startIndex = index + try { + while (chars[index] != '[') { + index += 1 + } + } catch (e: IndexOutOfBoundsException) { + throw SerializationException("Not a valid CQCode: $origin") + } + result.addLast(origin.substring(startIndex, index)) + index += 1 + } + } + } + + return@lazy result + } + + override fun decodeString() = origin + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + return Composited(this, items, serializersModule) + } + + private class Composited( + override val decoder: Decoder, + private val items: ArrayDeque, + override val serializersModule: SerializersModule, + ): ICQCodeDecoder.Composited { + private val log by logger() + + private var elementIndex = 0 + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (elementIndex < 0) { + throw IllegalStateException("Call beginStructure() first") + } + if (elementIndex >= items.size) { + return CompositeDecoder.DECODE_DONE + } + return elementIndex++ + } + + override fun endStructure(descriptor: SerialDescriptor) { + elementIndex = -1 + } + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String { + return items[index] + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T = throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageEncoder.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageEncoder.kt new file mode 100644 index 0000000..6df6f98 --- /dev/null +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageEncoder.kt @@ -0,0 +1,76 @@ +package io.github.mystere.serialization.cqcode + +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.listSerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule + +class CQCodeMessageEncoder( + override val serializersModule: SerializersModule, +): ICQCodeEncoder { + fun encodeCQCodeMessage(value: CQCodeMessage) { + with(beginStructure(listSerialDescriptor())) { + for (index in 0 until value.size) { + when (val item = value[index]) { + is CQCodeMessageItem.Text -> encodeStringElement(serialDescriptor(), index, item.text) + else -> { + val encoder = CQCodeMessageItemEncoder(serializersModule) + item._type.serialize(encoder, item) + encodeStringElement(serialDescriptor(), index, encoder.encodeFinalResult()) + } + } + } + } + } + + + private val items = HashMap() + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + items.clear() + return Composited(this, items, serializersModule) + } + + override fun encodeString(value: String) { + items.clear() + items[-1] = value + } + + fun encodeFinalResult(): String { + if (items.size == 1 && items.containsKey(-1)) { + return items[-1]!! + } + return StringBuilder().also { + for (index in 0 until items.size) { + it.append(items[index]!!) + } + }.toString() + } + + private class Composited( + private val encoder: Encoder, + private val items: HashMap, + override val serializersModule: SerializersModule, + ): ICQCodeEncoder.Composited { + private var elementIndex = 0 + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int) = encoder + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + items[index] = value + } + + override fun endStructure(descriptor: SerialDescriptor) { + elementIndex = -1 + } + + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + value?.let { encodeStringElement(descriptor, index, it.toString()) } + } + } +} \ No newline at end of file diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItem.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItem.kt index 6e629e5..abc389b 100644 --- a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItem.kt +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItem.kt @@ -1,72 +1,93 @@ package io.github.mystere.serialization.cqcode import kotlinx.serialization.* -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.decodeStructure -import kotlinx.serialization.encoding.encodeStructure import kotlinx.serialization.json.* /** * CQ 码元素 * @see CQ 码 / CQ Code | go-cqhttp 帮助中心 */ -@Serializable -sealed interface CQCodeMessageItem { +interface CQCodeMessageItem { @Transient - val type: Type + val _type: Type // TODO: 这个地方非常不优雅,暂时想不到优化方案,等待大佬 pr。。。 - @Serializable(with = CQCodeMessageItemTypeSerializer::class) enum class Type( val decodeFromJsonElement: (json: Json, element: JsonElement) -> CQCodeMessageItem, + val encodeToJsonElement: (json: Json, value: CQCodeMessageItem) -> JsonElement, val deserialize: (decoder: Decoder) -> CQCodeMessageItem, val serialize: (encoder: Encoder, value: CQCodeMessageItem) -> Unit, ) { - Text( + text( decodeFromJsonElement = { json, element -> - json.decodeFromJsonElement(CQCodeMessageItem.Text.serializer(), element) + json.decodeFromJsonElement(Text.serializer(), element) + }, + encodeToJsonElement = { json, value -> + json.encodeToJsonElement(Text.serializer(), value as Text) }, deserialize = { decoder -> - CQCodeMessageItem.Text.serializer().deserialize(decoder) + Text.serializer().deserialize(decoder) }, serialize = { encoder, value -> - CQCodeMessageItem.Text.serializer().serialize(encoder, value as CQCodeMessageItem.Text) + Text.serializer().serialize(encoder, value as Text) }, ), - Face( + face( decodeFromJsonElement = { json, element -> - json.decodeFromJsonElement(CQCodeMessageItem.Text.serializer(), element) + json.decodeFromJsonElement(Face.serializer(), element) + }, + encodeToJsonElement = { json, value -> + json.encodeToJsonElement(Face.serializer(), value as Face) }, deserialize = { decoder -> - CQCodeMessageItem.Face.serializer().deserialize(decoder) + Face.serializer().deserialize(decoder) }, serialize = { encoder, value -> - CQCodeMessageItem.Face.serializer().serialize(encoder, value as CQCodeMessageItem.Face) + Face.serializer().serialize(encoder, value as Face) }, ), - Record( + image( decodeFromJsonElement = { json, element -> - json.decodeFromJsonElement(CQCodeMessageItem.Text.serializer(), element) + json.decodeFromJsonElement(Image.serializer(), element) + }, + encodeToJsonElement = { json, value -> + json.encodeToJsonElement(Image.serializer(), value as Image) }, deserialize = { decoder -> - CQCodeMessageItem.Record.serializer().deserialize(decoder) + Image.serializer().deserialize(decoder) }, serialize = { encoder, value -> - CQCodeMessageItem.Record.serializer().serialize(encoder, value as CQCodeMessageItem.Record) + Image.serializer().serialize(encoder, value as Image) }, ), - Video( + record( decodeFromJsonElement = { json, element -> - json.decodeFromJsonElement(CQCodeMessageItem.Text.serializer(), element) + json.decodeFromJsonElement(Record.serializer(), element) + }, + encodeToJsonElement = { json, value -> + json.encodeToJsonElement(Record.serializer(), value as Record) }, deserialize = { decoder -> - CQCodeMessageItem.Video.serializer().deserialize(decoder) + Record.serializer().deserialize(decoder) }, serialize = { encoder, value -> - CQCodeMessageItem.Video.serializer().serialize(encoder, value as CQCodeMessageItem.Video) + Record.serializer().serialize(encoder, value as Record) + }, + ), + video( + decodeFromJsonElement = { json, element -> + json.decodeFromJsonElement(Video.serializer(), element) + }, + encodeToJsonElement = { json, value -> + json.encodeToJsonElement(Video.serializer(), value as Video) + }, + deserialize = { decoder -> + Video.serializer().deserialize(decoder) + }, + serialize = { encoder, value -> + Video.serializer().serialize(encoder, value as Video) }, ), ; @@ -91,7 +112,7 @@ sealed interface CQCodeMessageItem { @SerialName("text") val text: String ): CQCodeMessageItem { - override val type: Type = Type.Text + override val _type: Type = Type.text } // QQ 表情 @@ -100,7 +121,22 @@ sealed interface CQCodeMessageItem { @SerialName("id") val id: Long, ): CQCodeMessageItem { - override val type: Type = Type.Face + override val _type: Type = Type.face + } + + // 图片 + @Serializable + data class Image( + @SerialName("file") + val file: String, + @SerialName("type") + val type: Type? = null, + ): CQCodeMessageItem { + override val _type: CQCodeMessageItem.Type = CQCodeMessageItem.Type.image + + enum class Type { + flash, + } } // 语音 @@ -115,7 +151,7 @@ sealed interface CQCodeMessageItem { @SerialName("cache") val cache: Int? = null, ): CQCodeMessageItem { - override val type: Type = Type.Record + override val _type: Type = Type.record } // 短视频 @@ -124,36 +160,39 @@ sealed interface CQCodeMessageItem { @SerialName("file") val file: String, ): CQCodeMessageItem { - override val type: Type = Type.Video + override val _type: Type = Type.video } } - -private val types: Map by lazy { - with(hashMapOf()) { - for (value in CQCodeMessageItem.Type.entries) { - put(value.name.lowercase(), value) - } - return@with this - } -} -internal val String.asCQCodeMessageItemType: CQCodeMessageItem.Type get() { - return with(lowercase()) { - types[this] ?: throw SerializationException("Not a valid CQCodeMessageItem.Type: $this") - } +fun CQCodeMessageItem.asMessage(): CQCodeMessage { + return CQCodeMessage(ArrayDeque(listOf(this))) } -object CQCodeMessageItemTypeSerializer: KSerializer { - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - override val descriptor: SerialDescriptor = buildSerialDescriptor( - "io.github.mystere.serialization.cqcode.CQCodeMessageItem.Type", - SerialKind.ENUM, - ) - override fun deserialize(decoder: Decoder): CQCodeMessageItem.Type { - return decoder.decodeString().asCQCodeMessageItemType - } - - override fun serialize(encoder: Encoder, value: CQCodeMessageItem.Type) { - encoder.encodeString(value.name.lowercase()) - } -} +//private val types: Map by lazy { +// with(hashMapOf()) { +// for (value in CQCodeMessageItem.Type.entries) { +// put(value.name.lowercase(), value) +// } +// return@with this +// } +//} +//internal val String.asCQCodeMessageItemType: CQCodeMessageItem.Type get() { +// return with(lowercase()) { +// types[this] ?: throw SerializationException("Not a valid CQCodeMessageItem.Type: $this") +// } +//} +//object CQCodeMessageItemTypeSerializer: KSerializer { +// @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) +// override val descriptor: SerialDescriptor = buildSerialDescriptor( +// "io.github.mystere.serialization.cqcode.CQCodeMessageItem.Type", +// SerialKind.ENUM, +// ) +// +// override fun deserialize(decoder: Decoder): CQCodeMessageItem.Type { +// return decoder.decodeString().asCQCodeMessageItemType +// } +// +// override fun serialize(encoder: Encoder, value: CQCodeMessageItem.Type) { +// encoder.encodeString(value.name.lowercase()) +// } +//} diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemDecoder.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemDecoder.kt index 680d7e1..5cbdfa1 100644 --- a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemDecoder.kt +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemDecoder.kt @@ -2,7 +2,6 @@ package io.github.mystere.serialization.cqcode import io.github.mystere.util.logger import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder @@ -10,9 +9,24 @@ import kotlinx.serialization.modules.SerializersModule class CQCodeMessageItemDecoder( private val origin: String, - private val args: Map, + private val type: CQCodeMessageItem.Type, override val serializersModule: SerializersModule, -): ICQCodeMessageItemDecoder { +): ICQCodeDecoder { + private val log by logger() + + private val args: Map by lazy { + with(origin.substring(type.name.length + 5, origin.length - 1)) { + HashMap().also { + for (item in split(",")) { + if (!item.contains("=")) { + continue + } + val keyValue = item.split("=") + it[keyValue[0]] = keyValue[1] + } + } + } + } override fun decodeString() = origin override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { @@ -20,10 +34,10 @@ class CQCodeMessageItemDecoder( } private class Composited( - private val decoder: Decoder, + override val decoder: Decoder, private val args: Map, override val serializersModule: SerializersModule, - ): ICQCodeMessageItemDecoder.Composited { + ): ICQCodeDecoder.Composited { private val log by logger() private var elementIndex = 0 @@ -32,8 +46,6 @@ class CQCodeMessageItemDecoder( elementIndex = -1 } - override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = decoder - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { var name: String do { @@ -64,78 +76,8 @@ class CQCodeMessageItemDecoder( ): T { val name = descriptor.getElementName(index) return deserializer.deserialize(CQCodeMessageItemDecoder( - args[name]!!, mapOf(), serializersModule + args[name]!!, CQCodeMessageItem.Type.text, serializersModule )) } } } - -interface ICQCodeMessageItemDecoder: Decoder { - fun decodeUnsupported(): Nothing = throw UnsupportedOperationException() - override fun decodeBoolean() = decodeUnsupported() - override fun decodeByte() = decodeUnsupported() - override fun decodeChar() = decodeUnsupported() - override fun decodeDouble() = decodeUnsupported() - override fun decodeEnum(enumDescriptor: SerialDescriptor) = decodeUnsupported() - override fun decodeFloat() = decodeUnsupported() - override fun decodeInline(descriptor: SerialDescriptor) = this - override fun decodeInt() = decodeUnsupported() - override fun decodeLong() = decodeUnsupported() - @ExperimentalSerializationApi - override fun decodeNotNullMark() = decodeUnsupported() - @ExperimentalSerializationApi - override fun decodeNull(): Nothing? = null - override fun decodeShort() = decodeUnsupported() - override fun decodeString(): String - - interface Composited: CompositeDecoder { - override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean { - return decodeStringElement(descriptor, index).toBoolean() - } - - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { - return decodeStringElement(descriptor, index).toByte() - } - - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { - return decodeStringElement(descriptor, index).toCharArray()[0] - } - - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double { - return decodeStringElement(descriptor, index).toDouble() - } - - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { - return decodeStringElement(descriptor, index).toFloat() - } - - override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int { - return decodeStringElement(descriptor, index).toInt() - } - - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { - return decodeStringElement(descriptor, index).toLong() - } - - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { - return decodeStringElement(descriptor, index).toShort() - } - - @ExperimentalSerializationApi - override fun decodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T? { - return decodeSerializableElement(descriptor, index, deserializer, previousValue) - } - - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T - } -} diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemEncoder.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemEncoder.kt index 97e217f..e7e108e 100644 --- a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemEncoder.kt +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/CQCodeMessageItemEncoder.kt @@ -1,6 +1,5 @@ package io.github.mystere.serialization.cqcode -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.* @@ -8,7 +7,7 @@ import kotlinx.serialization.modules.SerializersModule class CQCodeMessageItemEncoder( override val serializersModule: SerializersModule, -): ICQCodeMessageItemEncoder { +): ICQCodeEncoder { private val args = hashMapOf() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { args.clear() @@ -25,7 +24,7 @@ class CQCodeMessageItemEncoder( return args[""]!! } return StringBuilder().also { - it.append("[CQ:${args["type"]}") + it.append("[CQ:${args["_type"]}") for ((key, value) in args) { it.append(",$key=$value") } @@ -37,7 +36,7 @@ class CQCodeMessageItemEncoder( private val encoder: Encoder, private val args: HashMap, override val serializersModule: SerializersModule, - ): ICQCodeMessageItemEncoder.Composited { + ): ICQCodeEncoder.Composited { private var elementIndex = 0 override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int) = encoder override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { @@ -58,73 +57,3 @@ class CQCodeMessageItemEncoder( } } } - -interface ICQCodeMessageItemEncoder: Encoder { - fun encodeUnsupported(): Nothing = throw UnsupportedOperationException() - override fun encodeBoolean(value: Boolean) = encodeUnsupported() - override fun encodeByte(value: Byte) = encodeUnsupported() - override fun encodeChar(value: Char) = encodeUnsupported() - override fun encodeDouble(value: Double) = encodeUnsupported() - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = encodeUnsupported() - override fun encodeFloat(value: Float) = encodeUnsupported() - override fun encodeInline(descriptor: SerialDescriptor) = this - override fun encodeInt(value: Int) = encodeUnsupported() - override fun encodeLong(value: Long) = encodeUnsupported() - @ExperimentalSerializationApi - override fun encodeNotNullMark() = encodeUnsupported() - @ExperimentalSerializationApi - override fun encodeNull() { } - override fun encodeShort(value: Short) = encodeUnsupported() - override fun encodeString(value: String) - - interface Composited: CompositeEncoder { - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - encodeStringElement(descriptor, index, value.toString()) - } - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - throw UnsupportedOperationException() - } - - @ExperimentalSerializationApi - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) { - encodeSerializableElement(descriptor, index, serializer, value) - } - } -} diff --git a/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/ICoder.kt b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/ICoder.kt new file mode 100644 index 0000000..724581f --- /dev/null +++ b/kotlinx-serialization-cqcode/src/commonMain/kotlin/io/github/mystere/serialization/cqcode/ICoder.kt @@ -0,0 +1,165 @@ +package io.github.mystere.serialization.cqcode + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + + +interface ICQCodeEncoder: Encoder { + fun encodeUnsupported(): Nothing = throw UnsupportedOperationException() + override fun encodeBoolean(value: Boolean) = encodeUnsupported() + override fun encodeByte(value: Byte) = encodeUnsupported() + override fun encodeChar(value: Char) = encodeUnsupported() + override fun encodeDouble(value: Double) = encodeUnsupported() + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = encodeUnsupported() + override fun encodeFloat(value: Float) = encodeUnsupported() + override fun encodeInline(descriptor: SerialDescriptor) = this + override fun encodeInt(value: Int) = encodeUnsupported() + override fun encodeLong(value: Long) = encodeUnsupported() + @ExperimentalSerializationApi + override fun encodeNotNullMark() = encodeUnsupported() + @ExperimentalSerializationApi + override fun encodeNull() { } + override fun encodeShort(value: Short) = encodeUnsupported() + override fun encodeString(value: String) + + interface Composited: CompositeEncoder { + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + encodeStringElement(descriptor, index, value.toString()) + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + throw UnsupportedOperationException() + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + encodeNullableSerializableElement(descriptor, index, serializer, value) + } + } +} + + +interface ICQCodeDecoder: Decoder { + fun decodeUnsupported(): Nothing = throw UnsupportedOperationException() + override fun decodeBoolean() = decodeString().toBoolean() + override fun decodeByte() = decodeString().toByte() + override fun decodeChar() = decodeString().toCharArray()[0] + override fun decodeDouble() = decodeString().toDouble() + override fun decodeFloat() = decodeString().toFloat() + override fun decodeInline(descriptor: SerialDescriptor) = this + override fun decodeInt() = decodeString().toInt() + override fun decodeLong() = decodeString().toLong() + override fun decodeShort() = decodeUnsupported() + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + val current = decodeString() + for (index in 0 until enumDescriptor.elementsCount) { + if (enumDescriptor.getElementName(index) == current) { + return index + } + } + return -1 + } + @ExperimentalSerializationApi + override fun decodeNotNullMark() = decodeUnsupported() + @ExperimentalSerializationApi + override fun decodeNull(): Nothing? = null + + override fun decodeString(): String + + interface Composited: CompositeDecoder { + val decoder: Decoder + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = decoder + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean { + return decodeStringElement(descriptor, index).toBoolean() + } + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { + return decodeStringElement(descriptor, index).toByte() + } + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { + return decodeStringElement(descriptor, index).toCharArray()[0] + } + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double { + return decodeStringElement(descriptor, index).toDouble() + } + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { + return decodeStringElement(descriptor, index).toFloat() + } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int { + return decodeStringElement(descriptor, index).toInt() + } + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { + return decodeStringElement(descriptor, index).toLong() + } + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { + return decodeStringElement(descriptor, index).toShort() + } + + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + return decodeSerializableElement(descriptor, index, deserializer, previousValue) + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T + } +} diff --git a/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/CQCodeTest.kt b/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/CQCodeTest.kt index 5123ac7..dfed7f9 100644 --- a/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/CQCodeTest.kt +++ b/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/CQCodeTest.kt @@ -1,18 +1,24 @@ package io.github.mystere.serialization.cqcode import kotlin.test.Test -import kotlin.test.assertIs -import kotlin.test.assertTrue -import kotlin.test.assertEquals class CQCodeTest { @Test - fun singleFaceTest() { - fromString("[CQ:face,id=142]") { - assertSize(1) - assertEquals(0, CQCodeMessageItem.Face( - id = 142L - )) - } - } + fun faceTest() = Test( + "faceTest", + CQCodeMessageItem.Face( + id = 142L + ) + ) + + @Test + fun imageTest() = Test( + "imageTest", + CQCodeMessageItem.Image( + file = "http://baidu.com/1.jpg" + ) + CQCodeMessageItem.Image( + file = "http://baidu.com/2.jpg", + type = CQCodeMessageItem.Image.Type.flash, + ) + ) } \ No newline at end of file diff --git a/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/Util.kt b/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/Util.kt index ee2a571..6b679f8 100644 --- a/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/Util.kt +++ b/kotlinx-serialization-cqcode/src/commonTest/kotlin/io/github/mystere/serialization/cqcode/Util.kt @@ -1,5 +1,6 @@ package io.github.mystere.serialization.cqcode +import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -7,6 +8,22 @@ fun fromString(string: String, block: CQCodeMessage.() -> Unit) { with(CQCode.decodeFromString(string), block) } +fun Test(testName: String, item: CQCodeMessageItem) { + Test(testName, item.asMessage()) +} +fun Test(testName: String, origin: CQCodeMessage) { + println("================== $testName ==================") + println("origin message: $origin") + val string = CQCode.encodeToString(origin) + println("encode string result: $string") + val json = CQCode.encodeToJson(origin) + println("encode json result: $json") + val item = CQCode.decodeFromString(string) + println("decode result: $item") + println("================== $testName ==================\n") + assertEquals(origin, item, "origin item is $origin, but decode result is $item") +} + fun CQCodeMessage.assertSize(size: Int) { kotlin.with(this@assertSize.size) { assertTrue("items size is not $size, but $this") { this == size } diff --git a/mystere-core/build.gradle.kts b/mystere-core/build.gradle.kts index e96c8be..41016e9 100644 --- a/mystere-core/build.gradle.kts +++ b/mystere-core/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) @@ -40,6 +42,7 @@ kotlin { implementation(mystere.kotlinx.coroutines.core) implementation(project(":mystere-util")) + implementation(project(":onebot-api")) } } @@ -112,6 +115,7 @@ buildkonfig { packageName = findProperty("mystere.lib.core.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/mystere-core/src/commonMain/kotlin/io/github/mystere/core/MystereBotConfig.kt b/mystere-core/src/commonMain/kotlin/io/github/mystere/core/MystereBotConfig.kt index 5d6e38c..907fbe5 100644 --- a/mystere-core/src/commonMain/kotlin/io/github/mystere/core/MystereBotConfig.kt +++ b/mystere-core/src/commonMain/kotlin/io/github/mystere/core/MystereBotConfig.kt @@ -1,10 +1,12 @@ package io.github.mystere.core +import io.github.mystere.onebot.IOneBotConnection + interface IMystereBot { val botId: String - fun connect() + fun connect(connection: IOneBotConnection) fun disconnect() override fun equals(other: Any?): Boolean diff --git a/mystere-qq/build.gradle.kts b/mystere-qq/build.gradle.kts index c561f17..7d5083e 100644 --- a/mystere-qq/build.gradle.kts +++ b/mystere-qq/build.gradle.kts @@ -1,9 +1,12 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) alias(mystere.plugins.ksp) alias(mystere.plugins.ktorfit) alias(mystere.plugins.buildkonfig) + alias(mystere.plugins.sqldelight) } kotlin { @@ -19,7 +22,7 @@ kotlin { macosX64() // linuxArm64() linuxX64() - mingwX64() +// mingwX64() applyDefaultHierarchyTemplate() @@ -41,6 +44,10 @@ kotlin { implementation(mystere.kotlinx.coroutines.core) implementation(project(":mystere-core")) + implementation(project(":mystere-util")) + + implementation(project(":onebot-api")) + implementation(project(":onebot-v11")) } } @@ -49,6 +56,7 @@ kotlin { dependencies { implementation(mystere.ktor.client.cio) implementation(mystere.slf4j.api) + implementation(mystere.sqldelight.driver.sqlite.jvm) } } @@ -87,10 +95,17 @@ kotlin { } // windows - val mingwX64Main by getting { - dependsOn(commonMain) +// val mingwX64Main by getting { +// dependsOn(commonMain) +// dependencies { +// implementation(mystere.ktor.client.winhttp) +// } +// } + + // native + val nativeMain by getting { dependencies { - implementation(mystere.ktor.client.winhttp) + implementation(mystere.sqldelight.driver.sqlite.native) } } } @@ -105,7 +120,7 @@ dependencies { add("kspMacosArm64", this) add("kspMacosX64", this) add("kspMacosArm64", this) - add("kspMingwX64", this) +// add("kspMingwX64", this) } } @@ -113,6 +128,16 @@ buildkonfig { packageName = findProperty("mystere.lib.qq.pkgName")!!.toString() defaultConfigs { + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) + } +} +sqldelight { + databases { + val MystereQQDatabase by creating { + packageName.set("io.github.mystere.sqlite") + generateAsync.set(true) + } } } diff --git a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/QQBot.kt b/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/QQBot.kt index 7ca2fe7..366a577 100644 --- a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/QQBot.kt +++ b/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/QQBot.kt @@ -1,6 +1,7 @@ package io.github.mystere.qq import io.github.mystere.core.IMystereBot +import io.github.mystere.onebot.IOneBotConnection import io.github.mystere.qq.qqapi.dto.AppAccessTokenReqDto import io.github.mystere.qq.qqapi.http.QQAuthAPI import io.github.mystere.qq.qqapi.http.QQBotAPI @@ -41,8 +42,12 @@ data class QQBot internal constructor( ) } - override fun connect() { + private var _OneBotConnection: IOneBotConnection? = null + private val OneBotConnection: IOneBotConnection get() = _OneBotConnection!! + + override fun connect(connection: IOneBotConnection) { scope.launch(Dispatchers.IO) { + _OneBotConnection = connection while (true) { if (accessTokenExpire >= 0) { delay(max(accessTokenExpire - 55, accessTokenExpire) * 1000L) @@ -67,8 +72,8 @@ data class QQBot internal constructor( websocket = QQBotWebsocketConnection( log = log, url = QQBotAPI.gateway().url, - ) { - accessToken + ) provider@{ + return@provider accessToken } } } diff --git a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/qqapi/websocket/QQBotWebsocketConnection.kt b/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/qqapi/websocket/QQBotWebsocketConnection.kt index e04a2b5..15bfe99 100644 --- a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/qqapi/websocket/QQBotWebsocketConnection.kt +++ b/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/qqapi/websocket/QQBotWebsocketConnection.kt @@ -1,15 +1,15 @@ package io.github.mystere.qq.qqapi.websocket +import io.github.mystere.core.Platform +import io.github.mystere.qq.BuildKonfig import io.github.mystere.qq.qqapi.websocket.message.Intent import io.github.mystere.qq.qqapi.websocket.message.OpCode10 import io.github.mystere.qq.qqapi.websocket.message.OpCode2 -import io.github.mystere.qq.util.JsonGlobal -import io.github.mystere.qq.util.WebsocketClient -import io.github.mystere.qq.util.withLogging +import io.github.mystere.util.JsonGlobal +import io.github.mystere.util.WebsocketClient import io.github.oshai.kotlinlogging.KLogger import io.ktor.client.* import io.ktor.client.plugins.websocket.* -import io.ktor.serialization.kotlinx.* import io.ktor.websocket.* import kotlinx.coroutines.* import kotlinx.serialization.* @@ -30,15 +30,7 @@ class QQBotWebsocketConnection internal constructor( CoroutineScope(Dispatchers.IO) } - private val WebsocketClient: HttpClient by lazy { - WebsocketClient() - .config { - install(WebSockets) { - contentConverter = KotlinxWebsocketSerializationConverter(JsonGlobal) - } - } - .withLogging(log) - } + private val WebsocketClient: HttpClient by lazy { WebsocketClient() } private var s: Long? = null init { @@ -54,9 +46,8 @@ class QQBotWebsocketConnection internal constructor( token = "QQBot ${accessTokenProvider()}", intents = Intent.DEFAULT, properties = buildJsonObject { - // TODO: 搭配 buildkonfing 动态版本名 - put("\$client", JsonPrimitive("Mystere v${"1.0.0-alpha01"}")) - put("\$platform", JsonPrimitive(0)) + put("\$client", JsonPrimitive("Mystere v${BuildKonfig.VERSION_NAME}(${BuildKonfig.COMMIT})")) + put("\$platform", JsonPrimitive(Platform.name)) }, ), serializer = OpCode2.IdentifyPayload.serializer(), diff --git a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Log.kt b/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Log.kt deleted file mode 100644 index 56289e0..0000000 --- a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Log.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.mystere.qq.util - -import io.github.mystere.core.MystereCore -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.client.* -import io.ktor.client.plugins.logging.* - -fun HttpClient.withLogging( - tag: String = "HttpClient", -): HttpClient = config { - withLogging(KotlinLogging.logger(tag)) -} - -fun HttpClient.withLogging( - log: KLogger, -): HttpClient = config { - install(Logging) { - level = if (MystereCore.Debug) LogLevel.ALL else LogLevel.HEADERS - logger = object : Logger { - override fun log(message: String) { - log.debug { message } - } - } - } -} diff --git a/mystere-qq/src/main/sqldelight/io/github/mystere/sqlite/MystereQQDatabase.sq b/mystere-qq/src/main/sqldelight/io/github/mystere/sqlite/MystereQQDatabase.sq new file mode 100644 index 0000000..e69de29 diff --git a/mystere-util/build.gradle.kts b/mystere-util/build.gradle.kts index 45c8e9c..a2365ec 100644 --- a/mystere-util/build.gradle.kts +++ b/mystere-util/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.buildkonfig) @@ -25,6 +27,12 @@ kotlin { val commonMain by getting { dependencies { api(mystere.kotlin.logging) + implementation(mystere.ktor.client.core) + implementation(mystere.ktor.plugin.logging) + implementation(mystere.ktor.client.content.negotiation) + implementation(mystere.ktor.plugin.serialization.kotlinx.json) + implementation(mystere.kotlinx.serialization.core) + implementation(mystere.kotlinx.serialization.json) } } @@ -33,6 +41,7 @@ kotlin { dependencies { implementation(mystere.slf4j.api) implementation(mystere.logback.classic) + implementation(mystere.ktor.client.cio) } } @@ -49,7 +58,7 @@ kotlin { } val macosMain by getting { dependencies { - + implementation(mystere.ktor.client.cio) } } @@ -66,14 +75,14 @@ kotlin { } val linuxMain by getting { dependencies { - + implementation(mystere.ktor.client.cio) } } // windows - val mingwX64Main by getting { + val mingwMain by getting { dependencies { - + implementation(mystere.ktor.client.winhttp) } } } @@ -83,6 +92,7 @@ buildkonfig { packageName = findProperty("mystere.lib.util.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Ktor.kt b/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Ktor.kt similarity index 62% rename from mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Ktor.kt rename to mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Ktor.kt index 169919b..01b8cec 100644 --- a/mystere-qq/src/commonMain/kotlin/io/github/mystere/qq/util/_Ktor.kt +++ b/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Ktor.kt @@ -1,25 +1,33 @@ -package io.github.mystere.qq.util +package io.github.mystere.util import io.ktor.client.* import io.ktor.client.plugins.api.* import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.websocket.* import io.ktor.http.* +import io.ktor.serialization.kotlinx.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json expect fun HttpClient(): HttpClient -expect fun WebsocketClient(): HttpClient +expect fun _WebsocketClient(): HttpClient +fun WebsocketClient(config: WebSockets.Config.() -> Unit = { + contentConverter = KotlinxWebsocketSerializationConverter(JsonGlobal) +}): HttpClient = _WebsocketClient().config { + install(WebSockets, config) +} val JsonGlobal = Json { isLenient = true ignoreUnknownKeys = true encodeDefaults = true explicitNulls = true + useArrayPolymorphism = true } val HttpClient.withJsonContent: HttpClient get() { return config { - install(createClientPlugin("json-header") { + install(createClientPlugin("JsonContent") { onRequest { request, _ -> request.contentType(ContentType.Application.Json) } diff --git a/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Log.kt b/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Log.kt index a7e78e2..bcafa3d 100644 --- a/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Log.kt +++ b/mystere-util/src/commonMain/kotlin/io/github/mystere/util/_Log.kt @@ -1,6 +1,29 @@ package io.github.mystere.util +import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.* +import io.ktor.client.plugins.logging.* + +fun HttpClient.withLogging( + tag: String = "HttpClient", +): HttpClient = config { + withLogging(KotlinLogging.logger(tag)) +} + +fun HttpClient.withLogging( + log: KLogger, + debug: Boolean = false, +): HttpClient = config { + install(Logging) { + level = if (debug) LogLevel.ALL else LogLevel.HEADERS + logger = object : Logger { + override fun log(message: String) { + log.debug { message } + } + } + } +} fun Any.logger() = lazy { KotlinLogging.logger(this::class.qualifiedName!!) diff --git a/mystere-util/src/jvmMain/kotlin/io/github/mystere/util/_Ktor.jvm.kt b/mystere-util/src/jvmMain/kotlin/io/github/mystere/util/_Ktor.jvm.kt new file mode 100644 index 0000000..a3c84ce --- /dev/null +++ b/mystere-util/src/jvmMain/kotlin/io/github/mystere/util/_Ktor.jvm.kt @@ -0,0 +1,11 @@ +package io.github.mystere.util + +import io.ktor.client.* +import io.ktor.client.engine.cio.* + +actual fun HttpClient(): HttpClient = HttpClient(CIO) { + +} +actual fun _WebsocketClient(): HttpClient = HttpClient(CIO) { + +} \ No newline at end of file diff --git a/mystere-util/src/linuxMain/kotlin/io/github/mystere/util/_Ktor.linux.kt b/mystere-util/src/linuxMain/kotlin/io/github/mystere/util/_Ktor.linux.kt new file mode 100644 index 0000000..a3c84ce --- /dev/null +++ b/mystere-util/src/linuxMain/kotlin/io/github/mystere/util/_Ktor.linux.kt @@ -0,0 +1,11 @@ +package io.github.mystere.util + +import io.ktor.client.* +import io.ktor.client.engine.cio.* + +actual fun HttpClient(): HttpClient = HttpClient(CIO) { + +} +actual fun _WebsocketClient(): HttpClient = HttpClient(CIO) { + +} \ No newline at end of file diff --git a/mystere-util/src/macosMain/kotlin/io/github/mystere/util/_Ktor.darwin.kt b/mystere-util/src/macosMain/kotlin/io/github/mystere/util/_Ktor.darwin.kt new file mode 100644 index 0000000..03aef64 --- /dev/null +++ b/mystere-util/src/macosMain/kotlin/io/github/mystere/util/_Ktor.darwin.kt @@ -0,0 +1,12 @@ +package io.github.mystere.util + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.websocket.* + +actual fun HttpClient(): HttpClient = HttpClient(CIO) { + +} +actual fun _WebsocketClient(): HttpClient = HttpClient(CIO) { + +} \ No newline at end of file diff --git a/mystere-util/src/mingwMain/kotlin/io/github/mystere/util/_Ktor.mingw.kt b/mystere-util/src/mingwMain/kotlin/io/github/mystere/util/_Ktor.mingw.kt new file mode 100644 index 0000000..c5cebc3 --- /dev/null +++ b/mystere-util/src/mingwMain/kotlin/io/github/mystere/util/_Ktor.mingw.kt @@ -0,0 +1,11 @@ +package io.github.mystere.util + +import io.ktor.client.* +import io.ktor.client.engine.winhttp.* + +actual fun HttpClient(): HttpClient = HttpClient(WinHttp) { + +} +actual fun _WebsocketClient(): HttpClient = HttpClient(WinHttp) { + +} \ No newline at end of file diff --git a/mystere/build.gradle.kts b/mystere/build.gradle.kts index 9720204..71cba5a 100644 --- a/mystere/build.gradle.kts +++ b/mystere/build.gradle.kts @@ -21,7 +21,8 @@ kotlin { // TODO: clikt // linuxArm64(), linuxX64(), - mingwX64(), + // TODO: ktor-server +// mingwX64(), ).forEach { it.binaries.executable { baseName = "mystere" @@ -47,6 +48,7 @@ kotlin { implementation(mystere.yamlkt) implementation(project(":mystere-core")) + implementation(project(":mystere-util")) implementation(project(":mystere-qq")) implementation(project(":onebot-api")) @@ -103,11 +105,11 @@ kotlin { } // windows - val mingwX64Main by getting { - dependencies { - - } - } +// val mingwX64Main by getting { +// dependencies { +// +// } +// } } } @@ -122,7 +124,7 @@ buildkonfig { packageName = findProperty("mystere.app.pkgName")!!.toString() defaultConfigs { - buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", findProperty("mystere.app.version")!!.toString()) -// buildConfigField(FieldSpec.Type.STRING, "COMMIT", VersionGen.GIT_HEAD) + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_APP) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/mystere/src/commonMain/kotlin/io/github/mystere/app/Mystere.kt b/mystere/src/commonMain/kotlin/io/github/mystere/app/Mystere.kt index ae27884..2e95df3 100644 --- a/mystere/src/commonMain/kotlin/io/github/mystere/app/Mystere.kt +++ b/mystere/src/commonMain/kotlin/io/github/mystere/app/Mystere.kt @@ -5,7 +5,7 @@ import com.github.ajalt.clikt.parameters.options.* import io.github.mystere.app.util.runBlockingWithCancellation import io.github.mystere.core.IMystereBot import io.github.mystere.core.MystereCore -import io.github.mystere.core.util.logger +import io.github.mystere.util.logger import io.github.mystere.qq.QQBot import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.io.buffered @@ -47,27 +47,28 @@ object Mystere: CliktCommand(), AutoCloseable { private val bots = hashMapOf() override fun run() { + log.info { "Mystere v${BuildKonfig.VERSION_NAME}(${BuildKonfig.COMMIT}) starting..." } if (_debug || Config.debug) { MystereCore.forceDebug() } if (MystereCore.Debug) { - log.info { "Debug mode on!" } + log.debug { "Debug mode on!" } } if (Config.bots.isEmpty()) { log.warn { "No bot added!" } return } - for (bots in Config.bots) { - val type = bots.getStringOrNull("type") + for (bot in Config.bots) { + val type = bot.getStringOrNull("type") ?.lowercase() ?: continue when (type) { "qq" -> QQBot.create(Yaml.decodeFromString( QQBot.Config.serializer(), - bots.toString(), + bot["detail"].toString(), )) else -> continue }.let { - this.bots[it.botId] = it + bots[it.botId] = it } } for (service in Config.services) { diff --git a/onebot-api/build.gradle.kts b/onebot-api/build.gradle.kts index 06cac4b..90137b0 100644 --- a/onebot-api/build.gradle.kts +++ b/onebot-api/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) @@ -85,6 +87,7 @@ buildkonfig { packageName = findProperty("mystere.lib.onebot.api.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotConfig.kt b/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotConfig.kt index a2174e4..d8f1064 100644 --- a/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotConfig.kt +++ b/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotConfig.kt @@ -1,5 +1,21 @@ package io.github.mystere.onebot -interface OneBotConnection { - val url: String +interface IOneBotConnection { + suspend fun init() + suspend fun sendEvent(event: IOneBotEvent) + + + interface IConfig { + val url: String? + + fun createConnection(): IOneBotConnection + } } + +interface IOneBotAction { + interface Param { + val action: String + } +} + +interface IOneBotEvent diff --git a/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotException.kt b/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotException.kt new file mode 100644 index 0000000..80cbb7e --- /dev/null +++ b/onebot-api/src/commonMain/kotlin/io/github/mystere/onebot/OneBotException.kt @@ -0,0 +1,5 @@ +package io.github.mystere.onebot + +class OneBotConnectionException( + override val message: String +): RuntimeException(message) \ No newline at end of file diff --git a/onebot-v11/build.gradle.kts b/onebot-v11/build.gradle.kts index 2846277..55b25a0 100644 --- a/onebot-v11/build.gradle.kts +++ b/onebot-v11/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) @@ -17,7 +19,7 @@ kotlin { macosX64() // linuxArm64() linuxX64() - mingwX64() +// mingwX64() applyDefaultHierarchyTemplate() @@ -28,19 +30,27 @@ kotlin { implementation(mystere.kotlin.reflect) implementation(mystere.kotlin.stdlib) + implementation(mystere.ktor.client.core) + implementation(mystere.ktor.client.content.negotiation) + implementation(mystere.ktor.client.auth) implementation(mystere.ktor.plugin.logging) + implementation(mystere.ktor.plugin.serialization.kotlinx.json) + implementation(mystere.ktorfit.lib.light) implementation(mystere.kotlinx.coroutines.core) implementation(mystere.kotlinx.serialization.json) + implementation(mystere.kotlinx.datetime) implementation(project(":onebot-api")) + implementation(project(":mystere-util")) + implementation(project(":kotlinx-serialization-cqcode")) } } // jvm val jvmMain by getting { dependencies { - + implementation(mystere.ktor.client.cio) } } @@ -57,7 +67,7 @@ kotlin { } val macosMain by getting { dependencies { - + implementation(mystere.ktor.client.cio) } } @@ -74,16 +84,16 @@ kotlin { // } val linuxMain by getting { dependencies { - + implementation(mystere.ktor.client.cio) } } // windows - val mingwX64Main by getting { - dependencies { - - } - } +// val mingwX64Main by getting { +// dependencies { +// implementation(mystere.ktor.client.winhttp) +// } +// } } } @@ -91,6 +101,7 @@ buildkonfig { packageName = findProperty("mystere.lib.onebot.v11.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/IOneBotV11Connection.kt b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/IOneBotV11Connection.kt new file mode 100644 index 0000000..b672dd2 --- /dev/null +++ b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/IOneBotV11Connection.kt @@ -0,0 +1,36 @@ +package io.github.mystere.onebot.v11 + +import io.github.mystere.onebot.IOneBotConnection +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.websocket.* +import io.ktor.http.* +import io.ktor.util.* +import io.ktor.websocket.* +import kotlinx.coroutines.* + +internal abstract class IOneBotV11Connection: IOneBotConnection + +internal suspend fun HttpClient.WebsocketConnection(url: String): OneBotClientWebSocketSession { + return OneBotClientWebSocketSession( + webSocketSession { + this.url.takeFrom(Url(url)) + } + ) +} + +class OneBotClientWebSocketSession( + private val delegate: DefaultClientWebSocketSession +): DefaultWebSocketSession by delegate { + @OptIn(InternalAPI::class) + override fun start(negotiatedExtensions: List>) { + delegate.start(negotiatedExtensions) + + // 心跳 + launch(Dispatchers.IO) { + while (isActive) { + + } + } + } +} diff --git a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotAction.kt b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotAction.kt new file mode 100644 index 0000000..fdd560e --- /dev/null +++ b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotAction.kt @@ -0,0 +1,26 @@ +package io.github.mystere.onebot.v11 + +import io.github.mystere.onebot.IOneBotAction +import io.github.mystere.onebot.IOneBotEvent +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +internal data class IOneBotV11Action( + @SerialName("params") + val params: Param, + @SerialName("action") + val action: String = params.action, + @SerialName("echo") + val echo: String? = null, +): IOneBotAction { + interface Param: IOneBotAction.Param { + @Transient + override val action: String + } +} + +data object SendPrivateMsg: IOneBotV11Action.Param { + override val action: String = "send_private_msg" +} diff --git a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotConfig.kt b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotConfig.kt index 4917bcd..acfb318 100644 --- a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotConfig.kt +++ b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotConfig.kt @@ -1,11 +1,48 @@ package io.github.mystere.onebot.v11 -import io.github.mystere.onebot.OneBotConnection +import io.github.mystere.onebot.IOneBotConnection +import io.github.mystere.onebot.v11.connection.ReverseWebSocketConnection +import kotlinx.serialization.Serializable object OneBotV11Connection { + @Serializable data class ReverseWebSocket( + override val url: String? = null, + val apiUrl: String? = null, + val eventUrl: String? = null, + val reconnectInterval: Int = 3000, + ) : IOneBotConnection.IConfig { + override fun createConnection(): IOneBotConnection { + return ReverseWebSocketConnection(url, apiUrl, eventUrl, reconnectInterval) + } + } + + @Serializable + data class HttpPost( + override val url: String, + ) : IOneBotConnection.IConfig { + override fun createConnection(): IOneBotConnection { + TODO("Not yet implemented") + } + } + + @Serializable + data class WebSocket( + override val url: String, + val apiUrl: String? = null, + val eventUrl: String? = null, + ) : IOneBotConnection.IConfig { + override fun createConnection(): IOneBotConnection { + TODO("Not yet implemented") + } + } + + @Deprecated("不打算真正实现") + data class Http( override val url: String, - val apiUrl: String?, - val eventUrl: String?, - ) : OneBotConnection + ) : IOneBotConnection.IConfig { + override fun createConnection(): IOneBotConnection { + TODO("Not yet implemented") + } + } } diff --git a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotEvent.kt b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotEvent.kt new file mode 100644 index 0000000..7dde350 --- /dev/null +++ b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/OneBotEvent.kt @@ -0,0 +1,188 @@ +package io.github.mystere.onebot.v11 + +import io.github.mystere.onebot.IOneBotEvent +import io.github.mystere.serialization.cqcode.CQCodeMessage +import kotlinx.datetime.Clock +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface IOneBotV11Event: IOneBotEvent { + @SerialName("post_type") + val postType: PostType + @SerialName("self_id") + val selfId: Long + @SerialName("time") + val time: Long + + enum class PostType { + message, notice, request, meta_event; + } +} + + +/************ 消息(message) ***********/ + +enum class MessageType { + private, group, +} +enum class MessageSubType { + friend, group, other +} +enum class Sex { + male, female, unknown +} + +// 私聊消息 +@Serializable +data class MessagePrivate( + override val selfId: Long, + @SerialName("sub_type") + val subType: MessageSubType, + @SerialName("message_id") + val messageId: Int, + @SerialName("message") + val message: CQCodeMessage, + @SerialName("raw_message") + val rawIMessage: String, + @SerialName("font") + val font: Int, + @SerialName("sender") + val sender: Sender, + @SerialName("user_id") + val userId: Long = sender.userId, +): IOneBotV11Event { + @SerialName("message_type") + val messageType: MessageType = MessageType.private + override val postType: IOneBotV11Event.PostType = IOneBotV11Event.PostType.message + override val time: Long = Clock.System.now().toEpochMilliseconds() + + @Serializable + data class Sender( + @SerialName("user_id") + val userId: Long, + @SerialName("nickname") + val nickname: String? = null, + @SerialName("sex") + val sex: Sex? = null, + @SerialName("age") + val age: Int? = null, + ) +} + +// 消息 +@Serializable +data class Message( + override val selfId: Long, + @SerialName("sub_type") + val subType: MessageSubType, + @SerialName("message_id") + val messageId: Int, + @SerialName("message") + val message: CQCodeMessage, + @SerialName("raw_message") + val rawIMessage: String, + @SerialName("font") + val font: Int, + @SerialName("sender") + val sender: Sender, + @SerialName("user_id") + val userId: Long = sender.userId, +): IOneBotV11Event { + @SerialName("message_type") + val messageType: MessageType = MessageType.group + override val postType: IOneBotV11Event.PostType = IOneBotV11Event.PostType.message + override val time: Long = Clock.System.now().toEpochMilliseconds() + + @Serializable + data class Sender( + @SerialName("user_id") + val userId: Long, + @SerialName("card") + val card: String? = null, + @SerialName("nickname") + val nickname: String? = null, + @SerialName("sex") + val sex: Sex? = null, + @SerialName("age") + val age: Int? = null, + ) +} + + +/************ 元事件(meta) ***********/ + +enum class MetaEventType { + lifecycle, heartbeat +} + +// 生命周期 +enum class LifecycleSubType { + enable, disable, connect +} +@Serializable +data class MetaLifecycle( + override val selfId: Long, + @SerialName("sub_type") + val subType: LifecycleSubType, +): IOneBotV11Event { + @SerialName("meta_event_type") + val metaEventType: MetaEventType = MetaEventType.lifecycle + override val postType: IOneBotV11Event.PostType = IOneBotV11Event.PostType.meta_event + override val time: Long = Clock.System.now().toEpochMilliseconds() +} + +// 心跳 +@Serializable +data class HeartbeatStatus( + @SerialName("online") + val online: Boolean?, + @SerialName("good") + val good: Boolean, +) +@Serializable +data class MetaHeartbeat( + override val selfId: Long, + @SerialName("state") + val state: HeartbeatStatus, +): IOneBotV11Event { + @SerialName("meta_event_type") + val metaEventType: MetaEventType = MetaEventType.heartbeat + override val postType: IOneBotV11Event.PostType = IOneBotV11Event.PostType.meta_event + override val time: Long = Clock.System.now().toEpochMilliseconds() +} + + +/************ 通知事件(notice) ***********/ + +enum class NoticeType { + group_upload +} + +// 群文件上传 +@Serializable +data class FileMeta( + @SerialName("id") + val id: String, + @SerialName("name") + val name: String, + @SerialName("size") + val size: Long, + @SerialName("busid") + val busid: Long, +) +@Serializable +data class NoticeGroupFileUpload( + override val selfId: Long, + @SerialName("group_id") + val groupId: Long, + @SerialName("user_id") + val userId: Long, + @SerialName("file") + val file: FileMeta, +): IOneBotV11Event { + @SerialName("notice_type") + val noticeType: NoticeType = NoticeType.group_upload + override val postType: IOneBotV11Event.PostType = IOneBotV11Event.PostType.notice + override val time: Long = Clock.System.now().toEpochMilliseconds() +} \ No newline at end of file diff --git a/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/connection/ReverseWebSocket.kt b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/connection/ReverseWebSocket.kt new file mode 100644 index 0000000..ce3ad75 --- /dev/null +++ b/onebot-v11/src/commonMain/kotlin/io/github/mystere/onebot/v11/connection/ReverseWebSocket.kt @@ -0,0 +1,41 @@ +package io.github.mystere.onebot.v11.connection + +import io.github.mystere.onebot.IOneBotEvent +import io.github.mystere.onebot.OneBotConnectionException +import io.github.mystere.onebot.v11.IOneBotV11Connection +import io.github.mystere.onebot.v11.OneBotClientWebSocketSession +import io.github.mystere.onebot.v11.WebsocketConnection +import io.github.mystere.util.WebsocketClient +import io.ktor.client.* + +internal class ReverseWebSocketConnection( + private val url: String? = null, + private val apiUrl: String? = null, + private val eventUrl: String? = null, + private val reconnectInterval: Int = 3000, +): IOneBotV11Connection() { + private val WebsocketClient: HttpClient by lazy { WebsocketClient() } + + private var _UniWebsocket: OneBotClientWebSocketSession? = null + private val UniWebsocket: OneBotClientWebSocketSession get() = _UniWebsocket!! + + private var _ApiWebsocket: OneBotClientWebSocketSession? = null + private val ApiWebsocket: OneBotClientWebSocketSession get() = _ApiWebsocket!! + private var _EventWebsocket: OneBotClientWebSocketSession? = null + private val EventWebsocket: OneBotClientWebSocketSession get() = _EventWebsocket!! + + override suspend fun init() { + if (url != null) { + _UniWebsocket = WebsocketClient.WebsocketConnection(url) + } else if (apiUrl != null && eventUrl != null) { + _ApiWebsocket = WebsocketClient.WebsocketConnection(apiUrl) + _EventWebsocket = WebsocketClient.WebsocketConnection(eventUrl) + } else { + throw OneBotConnectionException("url not set or apiUrl and eventUrl both not set!") + } + } + + override suspend fun sendEvent(event: IOneBotEvent) { + + } +} \ No newline at end of file diff --git a/onebot-v12/build.gradle.kts b/onebot-v12/build.gradle.kts index 2846277..389ea49 100644 --- a/onebot-v12/build.gradle.kts +++ b/onebot-v12/build.gradle.kts @@ -1,3 +1,5 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + plugins { alias(mystere.plugins.kotlin.multiplatform) alias(mystere.plugins.kotlin.plugin.serialization) @@ -91,6 +93,7 @@ buildkonfig { packageName = findProperty("mystere.lib.onebot.v11.pkgName")!!.toString() defaultConfigs { - + buildConfigField(FieldSpec.Type.STRING, "VERSION_NAME", MYSTERE_LIB) + buildConfigField(FieldSpec.Type.STRING, "COMMIT", GIT_HEAD) } } diff --git a/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotConfig.kt b/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotConfig.kt index d8d96a8..e6b8fad 100644 --- a/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotConfig.kt +++ b/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotConfig.kt @@ -1,9 +1,34 @@ package io.github.mystere.onebot.v12 -import io.github.mystere.onebot.OneBotConnection +import io.github.mystere.onebot.IOneBotConnection +import kotlinx.serialization.Serializable object OneBotV12Connection { + @Serializable data class ReverseWebSocket( override val url: String, - ): OneBotConnection + val accessToken: String? = null, + val reconnectInterval: Int = 3000, + ) : IOneBotConnection.IConfig + + @Serializable + data class WebSocket( + override val url: String, + val accessToken: String, + ) : IOneBotConnection.IConfig + + @Serializable + data class HttpWebhook( + override val url: String, + val apiUrl: String?, + val eventUrl: String?, + ) : IOneBotConnection.IConfig + + @Serializable + @Deprecated("不打算真正实现") + data class Http( + override val url: String, + val apiUrl: String?, + val eventUrl: String?, + ) : IOneBotConnection.IConfig } diff --git a/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotEvent.kt b/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotEvent.kt new file mode 100644 index 0000000..69e1140 --- /dev/null +++ b/onebot-v12/src/commonMain/kotlin/io/github/mystere/onebot/v12/OneBotEvent.kt @@ -0,0 +1,5 @@ +package io.github.mystere.onebot.v12 + +object OneBotV12Event { + +}