diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c4974d9a..e3e7dbd2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -43,6 +43,14 @@ jobs: - name: Run all tests run: gradle assemble allTests --stacktrace --warning-mode all + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports-${{ runner.os }} + path: '**/build/reports/tests' + retention-days: 7 + publish-releases: name: Publish releases needs: run-test @@ -82,6 +90,17 @@ jobs: steps: # 检出仓库代码 - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.JAVA_VERSION }} + cache: 'gradle' + + - name: Publish releases + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: ${{ env.GRADLE_VERSION }} + arguments: createChangelog # Create gitHub release - name: Create Github Release diff --git a/build.gradle.kts b/build.gradle.kts index 90f200ad..162ce571 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,14 +63,17 @@ idea { } } - apiValidation { ignoredPackages.add("*.internal.*") this.ignoredProjects.addAll( listOf( "api-reader", - "intents-processor" + "intents-processor", + "dispatch-serializer-processor", + "webhook-server-ktor", + "webhook-server-spring", + "webhook-server-spring-webflux", ), ) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index cc875403..bcdf4944 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -30,6 +30,8 @@ val kotlinVersion: String = libs.versions.kotlin.get() dependencies { implementation(kotlin("gradle-plugin", kotlinVersion)) implementation(kotlin("serialization", kotlinVersion)) + // for plugin.spring + implementation(kotlin("allopen", kotlinVersion)) implementation(libs.bundles.dokka) // see https://github.com/gradle-nexus/publish-plugin diff --git a/buildSrc/src/main/kotlin/P.kt b/buildSrc/src/main/kotlin/P.kt index 1fef4329..b80c5220 100644 --- a/buildSrc/src/main/kotlin/P.kt +++ b/buildSrc/src/main/kotlin/P.kt @@ -56,8 +56,8 @@ object P { override val homepage: String get() = HOMEPAGE - const val VERSION = "4.0.2" - const val NEXT_VERSION = "4.0.3" + const val VERSION = "4.1.0" + const val NEXT_VERSION = "4.1.1" override val snapshotVersion = "$NEXT_VERSION-SNAPSHOT" override val version = if (isSnapshot()) snapshotVersion else VERSION diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b68fd2db..85d54509 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,17 @@ [versions] -kotlin = "2.0.10" +kotlin = "2.0.20" kotlinx-coroutines = "1.9.0" -kotlinx-serialization = "1.7.1" +kotlinx-serialization = "1.7.3" kotlinx-datetime = "0.6.1" dokka = "1.9.20" ktor = "2.3.12" -openjdk-jmh = "1.37" -log4j = "2.23.1" +log4j = "2.24.1" # simbot -simbot = "4.6.0" -suspendTransform = "0.9.0" +simbot = "4.6.1" +suspendTransform = "2.0.20-0.9.3" gradleCommon = "0.6.0" # ksp -ksp = "2.0.10-1.0.24" +ksp = "2.0.20-1.0.25" # https://square.github.io/kotlinpoet/ kotlinPoet = "1.18.1" # https://detekt.dev/docs/intro diff --git a/internal-processors/api-reader/build.gradle.kts b/internal-processors/api-reader/build.gradle.kts index 5f97a9cb..defe66ae 100644 --- a/internal-processors/api-reader/build.gradle.kts +++ b/internal-processors/api-reader/build.gradle.kts @@ -26,10 +26,10 @@ repositories { } kotlin { - jvmToolchain(11) + jvmToolchain(JVMConstants.KT_JVM_TARGET_VALUE) compilerOptions { javaParameters = true - jvmTarget.set(JvmTarget.JVM_11) + jvmTarget.set(JvmTarget.fromTarget(JVMConstants.KT_JVM_TARGET_VALUE.toString())) } } diff --git a/internal-processors/dispatch-serializer-processor/build.gradle.kts b/internal-processors/dispatch-serializer-processor/build.gradle.kts new file mode 100644 index 00000000..3e032d1e --- /dev/null +++ b/internal-processors/dispatch-serializer-processor/build.gradle.kts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(JVMConstants.KT_JVM_TARGET_VALUE) + compilerOptions { + javaParameters = true + jvmTarget.set(JvmTarget.fromTarget(JVMConstants.KT_JVM_TARGET_VALUE.toString())) + } +} + +configJavaCompileWithModule() + +dependencies { + api(libs.ksp) + api(libs.kotlinPoet.ksp) + testImplementation(kotlin("test-junit5")) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + diff --git a/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessor.kt b/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessor.kt new file mode 100644 index 00000000..1d213973 --- /dev/null +++ b/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessor.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package qg.internal.processors.dispatcherserializer + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.isAbstract +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.devtools.ksp.symbol.Modifier +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.writeTo +import java.time.Instant +import java.time.ZoneOffset + +private const val SIGNAL_DISPATCH_PKG = "love.forte.simbot.qguild.event" +private const val SIGNAL_DISPATCH_SIMPLE_NAME = "Signal.Dispatch" +private const val SIGNAL_DISPATCH_NAME = "$SIGNAL_DISPATCH_PKG.$SIGNAL_DISPATCH_SIMPLE_NAME" + +/** + * + * @author ForteScarlet + */ +internal class DispatchSerializerProcessor( + private val environment: SymbolProcessorEnvironment, +) : SymbolProcessor { + private data class SerializableDispatch( + val declaration: KSClassDeclaration, + val typeName: String + ) + + private lateinit var signalDispatchDeclaration: KSClassDeclaration + + private val dispatchSerializableDeclarations = sortedSetOf( + Comparator.comparing { it.typeName } + ) + + override fun process(resolver: Resolver): List { + signalDispatchDeclaration = resolver.getClassDeclarationByName(SIGNAL_DISPATCH_NAME) + ?: error("Cannot find class declaration $SIGNAL_DISPATCH_NAME") + + // find all subtypes, + resolver.getAllFiles() + .flatMap { it.declarations } + .filterIsInstance() + .filter { + signalDispatchDeclaration.asStarProjectedType().isAssignableFrom(it.asStarProjectedType()) + } + // not abstract + .filter { + !it.isAbstract() && !it.modifiers.contains(Modifier.SEALED) + } + // marked @kotlinx.serialization.Serializable + .filter { + it.annotations.any { anno -> + with(anno.annotationType.resolve().declaration) { + packageName.asString() == "kotlinx.serialization" && + simpleName.asString() == "Serializable" + } + } + } + .mapNotNull { + // find @love.forte.simbot.qguild.event.DispatchTypeName + val typeNameAnnotation = it.annotations.find { anno -> + with(anno.annotationType.resolve().declaration) { + packageName.asString() == "love.forte.simbot.qguild.event" && + simpleName.asString() == "DispatchTypeName" + } + } ?: return@mapNotNull null + + val argument = typeNameAnnotation.arguments.find { a -> a.name?.asString() == "value" } + ?: return@mapNotNull null + + val typeName = argument.value as String + + SerializableDispatch(it, typeName) + } + .toCollection(dispatchSerializableDeclarations) + + return emptyList() + } + + override fun finish() { + genResolver() + } + + private fun genResolver() { + val orgFiles = mutableListOf() + signalDispatchDeclaration.containingFile?.also(orgFiles::add) + + val fileSpecBuilder = FileSpec.builder("love.forte.simbot.qguild.event", "SignalDispatchResolvers.generated") + + val funBuilder = FunSpec.builder("resolveDispatchSerializer").apply { + addAnnotation(ClassName("love.forte.simbot.qguild", "Generated")) + addParameter("eventName", String::class) + returns( + ClassName("kotlinx.serialization", "KSerializer") + .parameterizedBy( + WildcardTypeName + .producerOf( + ClassName(SIGNAL_DISPATCH_PKG, SIGNAL_DISPATCH_SIMPLE_NAME) + ) + ) + .copy(nullable = true) + ) + + addCode( + CodeBlock.builder().apply { + beginControlFlow("return when(eventName)") + // "" -> Type.serializer() + for (dispatchSerializableDeclaration in dispatchSerializableDeclarations) { + dispatchSerializableDeclaration.declaration.containingFile?.also(orgFiles::add) + + addStatement( + "%S -> %T.serializer()", + dispatchSerializableDeclaration.typeName, + dispatchSerializableDeclaration.declaration.toClassName() + ) + } + addStatement("else -> null") + endControlFlow() + }.build() + ) + + addKdoc( + CodeBlock.builder().apply { + addStatement("| event name | target |") + addStatement("| --- | --- |") + for (dispatchSerializableDeclaration in dispatchSerializableDeclarations) { + addStatement( + "| `%S` | [%T] |", + dispatchSerializableDeclaration.typeName, + dispatchSerializableDeclaration.declaration.toClassName() + ) + } + addStatement("") + addStatement("@since 4.1.0") + }.build() + ) + } + + fileSpecBuilder.addFileComment("Auto generated at ${Instant.now().atOffset(ZoneOffset.ofHours(8))}") + fileSpecBuilder.addFunction(funBuilder.build()) + + fileSpecBuilder.build().writeTo( + environment.codeGenerator, + aggregating = true, + originatingKSFiles = orgFiles + ) + } +} diff --git a/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessorProvider.kt b/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessorProvider.kt new file mode 100644 index 00000000..3ab304a0 --- /dev/null +++ b/internal-processors/dispatch-serializer-processor/src/main/kotlin/qg/internal/processors/dispatcherserializer/DispatchSerializerProcessorProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package qg.internal.processors.dispatcherserializer + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +/** + * + * @author ForteScarlet + */ +class DispatchSerializerProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = + DispatchSerializerProcessor(environment) +} diff --git a/internal-processors/dispatch-serializer-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/internal-processors/dispatch-serializer-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000..4cae3785 --- /dev/null +++ b/internal-processors/dispatch-serializer-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +qg.internal.processors.dispatcherserializer.DispatchSerializerProcessorProvider diff --git a/internal-processors/intents-processor/build.gradle.kts b/internal-processors/intents-processor/build.gradle.kts index bc6c72cd..3e032d1e 100644 --- a/internal-processors/intents-processor/build.gradle.kts +++ b/internal-processors/intents-processor/build.gradle.kts @@ -26,10 +26,10 @@ repositories { } kotlin { - jvmToolchain(11) + jvmToolchain(JVMConstants.KT_JVM_TARGET_VALUE) compilerOptions { javaParameters = true - jvmTarget.set(JvmTarget.JVM_11) + jvmTarget.set(JvmTarget.fromTarget(JVMConstants.KT_JVM_TARGET_VALUE.toString())) } } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 68db82cd..ec6b9598 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -14,10 +14,10 @@ abort-controller@3.0.0: dependencies: event-target-shim "^5.0.0" -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://mirrors.cloud.tencent.com/npm/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-regex@^5.0.1: version "5.0.1" @@ -68,9 +68,9 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-stdout@1.3.1: +browser-stdout@^1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + resolved "https://mirrors.cloud.tencent.com/npm/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== buffer-from@^1.0.0: @@ -91,10 +91,10 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.5.3: + version "3.6.0" + resolved "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -127,22 +127,22 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -debug@4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@^4.3.5: + version "4.3.7" + resolved "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -diff@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff@^5.2.0: + version "5.2.0" + resolved "https://mirrors.cloud.tencent.com/npm/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== emoji-regex@^8.0.0: version "8.0.0" @@ -154,9 +154,9 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-string-regexp@4.0.0: +escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://mirrors.cloud.tencent.com/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== event-target-shim@^5.0.0: @@ -171,9 +171,9 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-up@5.0.0: +find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://mirrors.cloud.tencent.com/npm/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -211,9 +211,9 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@8.1.0: +glob@^8.1.0: version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + resolved "https://mirrors.cloud.tencent.com/npm/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -227,9 +227,9 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -he@1.2.0: +he@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + resolved "https://mirrors.cloud.tencent.com/npm/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== inflight@^1.0.4: @@ -284,13 +284,25 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -js-yaml@4.1.0: +js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + resolved "https://mirrors.cloud.tencent.com/npm/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" +libsodium-sumo@^0.7.13: + version "0.7.15" + resolved "https://registry.npmmirror.com/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz#91c1d863fe3fbce6d6b9db1aadaa622733a1d007" + integrity sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw== + +libsodium-wrappers-sumo@0.7.13: + version "0.7.13" + resolved "https://registry.npmmirror.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.13.tgz#a33aea845a0bb56db067548f04feba28c730ab8e" + integrity sha512-lz4YdplzDRh6AhnLGF2Dj2IUj94xRN6Bh8T0HLNwzYGwPehQJX6c7iYVrFUPZ3QqxE0bqC+K0IIqqZJYWumwSQ== + dependencies: + libsodium-sumo "^0.7.13" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -298,62 +310,50 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -log-symbols@4.1.0: +log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + resolved "https://mirrors.cloud.tencent.com/npm/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" is-unicode-supported "^0.1.0" -minimatch@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^5.0.1: +minimatch@^5.0.1, minimatch@^5.1.6: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -mocha@10.3.0: - version "10.3.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz#0e185c49e6dccf582035c05fa91084a4ff6e3fe9" - integrity sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg== - dependencies: - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.4" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "8.1.0" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "5.0.1" - ms "2.1.3" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - workerpool "6.2.1" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: +mocha@10.7.0: + version "10.7.0" + resolved "https://mirrors.cloud.tencent.com/npm/mocha/-/mocha-10.7.0.tgz#9e5cbed8fa9b37537a25bd1f7fb4f6fc45458b9a" + integrity sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== node-fetch@2.6.7: @@ -423,10 +423,10 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" @@ -459,18 +459,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-json-comments@3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -478,6 +471,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.1.1: + version "8.1.1" + resolved "https://mirrors.cloud.tencent.com/npm/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -490,10 +490,10 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -typescript@5.4.3: - version "5.4.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" - integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== +typescript@5.5.4: + version "5.5.4" + resolved "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== webidl-conversions@^3.0.0: version "3.0.1" @@ -508,10 +508,10 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -workerpool@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" - integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== +workerpool@^6.5.1: + version "6.5.1" + resolved "https://mirrors.cloud.tencent.com/npm/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== wrap-ansi@^7.0.0: version "7.0.0" @@ -537,19 +537,14 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + resolved "https://mirrors.cloud.tencent.com/npm/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-unparser@2.0.0: +yargs-unparser@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + resolved "https://mirrors.cloud.tencent.com/npm/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: camelcase "^6.0.0" @@ -557,9 +552,9 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@16.2.0: +yargs@^16.2.0: version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + resolved "https://mirrors.cloud.tencent.com/npm/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2" diff --git a/samples/webhook-server-ktor/build.gradle.kts b/samples/webhook-server-ktor/build.gradle.kts new file mode 100644 index 00000000..cc8f989b --- /dev/null +++ b/samples/webhook-server-ktor/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") + id("io.ktor.plugin") version "2.3.12" +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(JVMConstants.KT_JVM_TARGET_VALUE) + compilerOptions { + javaParameters = true + jvmTarget.set(JvmTarget.fromTarget(JVMConstants.KT_JVM_TARGET_VALUE.toString())) + } +} + +configJavaCompileWithModule() + +application { + mainClass.set("com.example.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +dependencies { + implementation(project(":simbot-component-qq-guild-core")) + implementation(libs.simbot.core) + + val ktorVersion = "2.3.12" + implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion") + + val logbackVersion = "1.4.14" + implementation("ch.qos.logback:logback-classic:$logbackVersion") + + testImplementation("io.ktor:ktor-server-test-host") + testImplementation(kotlin("test-junit5")) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + diff --git a/samples/webhook-server-ktor/src/main/kotlin/com/example/Application.kt b/samples/webhook-server-ktor/src/main/kotlin/com/example/Application.kt new file mode 100644 index 00000000..e00192d6 --- /dev/null +++ b/samples/webhook-server-ktor/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package com.example + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import love.forte.simbot.component.qguild.bot.emitEvent +import love.forte.simbot.component.qguild.filterIsQQGuildBotManagers +import love.forte.simbot.component.qguild.useQQGuild +import love.forte.simbot.core.application.launchSimpleApplication +import love.forte.simbot.qguild.stdlib.Ed25519SignatureVerification +import love.forte.simbot.qguild.stdlib.EmitResult + +private const val SIGNATURE_HEAD = "X-Signature-Ed25519" +private const val TIMESTAMP_HEAD = "X-Signature-Timestamp" + +// 你也可以考虑直接把注册好的 bot 保存起来,而不只是保存 application +// 或者使用一些DI方案,都可以。 +lateinit var simbotApplication: love.forte.simbot.application.Application + +suspend fun main() { + // 启动simbot application, + // 然后启动内嵌的 HTTP 服务 + // 当然,具体的启动顺序或逻辑根据你的项目需求而定。 + simbotApplication = launchSimbot() + + embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) + .start(wait = true) + +} + +/** + * 启动 simbot application + */ +suspend fun launchSimbot(): love.forte.simbot.application.Application { + val application = launchSimpleApplication { + useQQGuild() + } + + // 这里配置你的 bot、事件监听等... + // 你也可以考虑直接把注册好的 bot 保存起来,而不只是保存 application + + return application +} + +fun Application.module() { + configureRouting() +} + +fun Application.configureRouting() { + routing { + post("/callback/qq/{appId}") { + val appId = call.parameters["appId"] + + // 寻找指定 `appId` 的 QGBot + val targetBot = simbotApplication.botManagers + .filterIsQQGuildBotManagers() + .firstNotNullOfOrNull { + it.all().firstOrNull { bot -> + bot.source.ticket.appId == appId + } + } + + // 如果找不到,响应 404 异常 + if (targetBot == null) { + call.respond(HttpStatusCode.NotFound) + return@post + } + + // 准备参数 + val signature = call.request.header(SIGNATURE_HEAD) + ?: run { + call.respond( + HttpStatusCode.BadRequest, + "Required header $SIGNATURE_HEAD is missing" + ) + return@post + } + + val timestamp = call.request.header(TIMESTAMP_HEAD) + ?: run { + call.respond( + HttpStatusCode.BadRequest, + "Required header $TIMESTAMP_HEAD is missing" + ) + return@post + } + val payload = call.receiveText() + + val result = targetBot.emitEvent( + payload, + ) { + // 配置 ed25519SignatureVerification, 即代表进行签名校验 + ed25519SignatureVerification = Ed25519SignatureVerification( + signature, + timestamp + ) + } + + val respond: String? = when (result) { + is EmitResult.Verified -> + // 如果你安装了插件 ContentNegotiation, + // 那么也可以直接响应对象。 + // 这里懒得装了,所以提前序列化成JSON字符串 + Json.encodeToString(result.verified) + + else -> null + } + + call.respondText( + respond ?: "{}", + ContentType.Application.Json + ) + } + } +} diff --git a/samples/webhook-server-ktor/src/main/resources/logback.xml b/samples/webhook-server-ktor/src/main/resources/logback.xml new file mode 100644 index 00000000..bdbb64ec --- /dev/null +++ b/samples/webhook-server-ktor/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/samples/webhook-server-spring-webflux/build.gradle.kts b/samples/webhook-server-spring-webflux/build.gradle.kts new file mode 100644 index 00000000..fffcd935 --- /dev/null +++ b/samples/webhook-server-spring-webflux/build.gradle.kts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") + kotlin("plugin.spring") + id("org.springframework.boot") version "3.3.4" + id("io.spring.dependency-management") version "1.1.6" +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(17) + compilerOptions { + javaParameters = true + jvmTarget.set(JvmTarget.JVM_17) + } +} + +configJavaCompileWithModule(jvmVersion = "17") + +dependencies { + implementation(project(":simbot-component-qq-guild-core")) + implementation(libs.simbot.spring) + + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + + testImplementation("io.projectreactor:reactor-test") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation(kotlin("test-junit5")) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + diff --git a/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/Application.kt b/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/Application.kt new file mode 100644 index 00000000..bb023fd0 --- /dev/null +++ b/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package com.example + +import love.forte.simbot.spring.EnableSimbot +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@EnableSimbot +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/CallbackHandler.kt b/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/CallbackHandler.kt new file mode 100644 index 00000000..a6f2094e --- /dev/null +++ b/samples/webhook-server-spring-webflux/src/main/kotlin/com/example/CallbackHandler.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package com.example + +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import love.forte.simbot.application.Application +import love.forte.simbot.component.qguild.bot.emitEvent +import love.forte.simbot.component.qguild.filterIsQQGuildBotManagers +import love.forte.simbot.qguild.stdlib.Ed25519SignatureVerification +import love.forte.simbot.qguild.stdlib.EmitResult +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException +import java.util.concurrent.CompletableFuture + +private const val SIGNATURE_HEAD = "X-Signature-Ed25519" +private const val TIMESTAMP_HEAD = "X-Signature-Timestamp" + +/** + * 处理所有qq机器人的回调请求的处理器。 + */ +@RestController("/callback") +class CallbackHandler( + private val application: Application +) { + + /** + * 处理 `/callback/qq/{appId}` 的事件回调请求, + * 找到对应的 bot 并向其推送事件。 + */ + @PostMapping("/qq/{appId}") + fun handleEvent( + @PathVariable("appId") appId: String, + @RequestHeader(SIGNATURE_HEAD) signature: String, + @RequestHeader(TIMESTAMP_HEAD) timestamp: String, + @RequestBody payload: String, + ): CompletableFuture> { + // 寻找指定 `appId` 的 QGBot + val targetBot = application.botManagers.filterIsQQGuildBotManagers().firstNotNullOfOrNull { + it.all().firstOrNull { bot -> bot.source.ticket.appId == appId } + } + + // 如果找不到,响应 404 异常 + if (targetBot == null) { + throw ResponseStatusException( + HttpStatus.NOT_FOUND, + "app $appId not found" + ) + } + + // 在 servlet web 中,在异步中处理. + // 作用域、是否要用异步等根据你的项目情况调整。 + val entityAsync = application.async { + val result = targetBot.emitEvent( + payload, + ) { + // 配置 ed25519SignatureVerification, 即代表进行签名校验 + ed25519SignatureVerification = Ed25519SignatureVerification( + signature, + timestamp + ) + } + + val body: Any? = when (result) { + is EmitResult.Verified -> result.verified + else -> null + } + + // 响应结果。 + ResponseEntity.ok(body) + } + + return entityAsync.asCompletableFuture() + } + +} diff --git a/samples/webhook-server-spring-webflux/src/main/resources/application.yml b/samples/webhook-server-spring-webflux/src/main/resources/application.yml new file mode 100644 index 00000000..042d4d0e --- /dev/null +++ b/samples/webhook-server-spring-webflux/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + port: 8080 + +spring: + application: + name: sample + diff --git a/samples/webhook-server-spring/build.gradle.kts b/samples/webhook-server-spring/build.gradle.kts new file mode 100644 index 00000000..ba9228e5 --- /dev/null +++ b/samples/webhook-server-spring/build.gradle.kts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") + kotlin("plugin.spring") + id("org.springframework.boot") version "3.3.4" + id("io.spring.dependency-management") version "1.1.6" +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(17) + compilerOptions { + javaParameters = true + jvmTarget.set(JvmTarget.JVM_17) + } +} + +configJavaCompileWithModule(jvmVersion = "17") + +dependencies { + implementation(project(":simbot-component-qq-guild-core")) + implementation(libs.simbot.spring) + + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation(kotlin("test-junit5")) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + diff --git a/samples/webhook-server-spring/src/main/kotlin/com/example/Application.kt b/samples/webhook-server-spring/src/main/kotlin/com/example/Application.kt new file mode 100644 index 00000000..bb023fd0 --- /dev/null +++ b/samples/webhook-server-spring/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package com.example + +import love.forte.simbot.spring.EnableSimbot +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@EnableSimbot +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/webhook-server-spring/src/main/kotlin/com/example/CallbackHandler.kt b/samples/webhook-server-spring/src/main/kotlin/com/example/CallbackHandler.kt new file mode 100644 index 00000000..7734cae8 --- /dev/null +++ b/samples/webhook-server-spring/src/main/kotlin/com/example/CallbackHandler.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package com.example + +import love.forte.simbot.application.Application +import love.forte.simbot.component.qguild.bot.emitEvent +import love.forte.simbot.component.qguild.filterIsQQGuildBotManagers +import love.forte.simbot.qguild.stdlib.Ed25519SignatureVerification +import love.forte.simbot.qguild.stdlib.EmitResult +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException + +private const val SIGNATURE_HEAD = "X-Signature-Ed25519" +private const val TIMESTAMP_HEAD = "X-Signature-Timestamp" + +/** + * 处理所有qq机器人的回调请求的处理器。 + */ +@RestController("/callback") +class CallbackHandler( + private val application: Application +) { + /** + * 处理 `/callback/qq/{appId}` 的事件回调请求, + * 找到对应的 bot 并向其推送事件。 + */ + @PostMapping("/qq/{appId}") + suspend fun handleEvent( + @PathVariable("appId") appId: String, + @RequestHeader(SIGNATURE_HEAD) signature: String, + @RequestHeader(TIMESTAMP_HEAD) timestamp: String, + @RequestBody payload: String, + ): ResponseEntity { + // 寻找指定 `appId` 的 QGBot + val targetBot = application.botManagers + .filterIsQQGuildBotManagers() + .firstNotNullOfOrNull { + it.all().firstOrNull { bot -> + bot.source.ticket.appId == appId + } + } + + // 如果找不到,响应 404 异常 + if (targetBot == null) { + throw ResponseStatusException( + HttpStatus.NOT_FOUND, + "app $appId not found" + ) + } + + val result = targetBot.emitEvent( + payload, + ) { + // 配置 ed25519SignatureVerification, 即代表进行签名校验 + ed25519SignatureVerification = Ed25519SignatureVerification( + signature, + timestamp + ) + } + + val body: Any? = when (result) { + is EmitResult.Verified -> result.verified + else -> null + } + + // 响应结果 + return ResponseEntity.ok(body) + } +} diff --git a/samples/webhook-server-spring/src/main/resources/application.yml b/samples/webhook-server-spring/src/main/resources/application.yml new file mode 100644 index 00000000..042d4d0e --- /dev/null +++ b/samples/webhook-server-spring/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + port: 8080 + +spring: + application: + name: sample + diff --git a/settings.gradle.kts b/settings.gradle.kts index 211f45f9..23baae11 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,19 +19,16 @@ rootProject.name = "qq-guild" // internals include(":internal-processors:api-reader") +include(":internal-processors:dispatch-serializer-processor") include(":internal-processors:intents-processor") -//include(":builder-generator") include(":simbot-component-qq-guild-api") include(":simbot-component-qq-guild-stdlib") include(":simbot-component-qq-guild-core") -//include(":simbot-component-qq-guild-core") -//include(":simbot-component-qq-guild-benchmark") -// tests -//if (!System.getenv("IS_CI").toBoolean()) { -// include(":tests:application-test") -// include(":tests:spring-boot-test") -// include(":tests:plugin-test") -//} +// samples +include(":samples:webhook-server-ktor") +include(":samples:webhook-server-spring") +include(":samples:webhook-server-spring-webflux") + diff --git a/simbot-component-qq-guild-api/api/simbot-component-qq-guild-api.api b/simbot-component-qq-guild-api/api/simbot-component-qq-guild-api.api index abef67c5..8ab37d67 100644 --- a/simbot-component-qq-guild-api/api/simbot-component-qq-guild-api.api +++ b/simbot-component-qq-guild-api/api/simbot-component-qq-guild-api.api @@ -33,6 +33,9 @@ public final class love/forte/simbot/qguild/ErrInfo$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface annotation class love/forte/simbot/qguild/Generated : java/lang/annotation/Annotation { +} + public abstract interface annotation class love/forte/simbot/qguild/PrivateDomainOnly : java/lang/annotation/Annotation { } @@ -1592,8 +1595,6 @@ public final class love/forte/simbot/qguild/api/message/MessageSendApi$Body { public final fun component6 ()Ljava/lang/String; public final fun component7 ()Ljava/lang/String; public final fun component8 ()Llove/forte/simbot/qguild/model/Message$Markdown; - public final fun copy (Ljava/lang/String;Llove/forte/simbot/qguild/model/Message$Embed;Llove/forte/simbot/qguild/model/Message$Ark;Llove/forte/simbot/qguild/model/Message$Reference;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Llove/forte/simbot/qguild/model/Message$Markdown;)Llove/forte/simbot/qguild/api/message/MessageSendApi$Body; - public static synthetic fun copy$default (Llove/forte/simbot/qguild/api/message/MessageSendApi$Body;Ljava/lang/String;Llove/forte/simbot/qguild/model/Message$Embed;Llove/forte/simbot/qguild/model/Message$Ark;Llove/forte/simbot/qguild/model/Message$Reference;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Llove/forte/simbot/qguild/model/Message$Markdown;ILjava/lang/Object;)Llove/forte/simbot/qguild/api/message/MessageSendApi$Body; public fun equals (Ljava/lang/Object;)Z public final fun getArk ()Llove/forte/simbot/qguild/model/Message$Ark; public final fun getContent ()Ljava/lang/String; @@ -2436,6 +2437,10 @@ public final class love/forte/simbot/qguild/event/DirectMessageCreate$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface annotation class love/forte/simbot/qguild/event/DispatchTypeName : java/lang/annotation/Annotation { + public abstract fun value ()Ljava/lang/String; +} + public final class love/forte/simbot/qguild/event/EventChannel { public static final field Companion Llove/forte/simbot/qguild/event/EventChannel$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Llove/forte/simbot/qguild/model/ChannelType;Llove/forte/simbot/qguild/model/ChannelSubType;Ljava/lang/String;Ljava/lang/String;)V @@ -3714,6 +3719,10 @@ public abstract class love/forte/simbot/qguild/event/Opcode { public final fun isSend ()Z } +public final class love/forte/simbot/qguild/event/Opcode$CallbackVerify : love/forte/simbot/qguild/event/Opcode, love/forte/simbot/qguild/event/ReceiveAble { + public static final field INSTANCE Llove/forte/simbot/qguild/event/Opcode$CallbackVerify; +} + public final class love/forte/simbot/qguild/event/Opcode$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -3760,6 +3769,7 @@ public final class love/forte/simbot/qguild/event/Opcode$SerializerByCode : kotl } public final class love/forte/simbot/qguild/event/Opcodes { + public static final field CallbackVerify I public static final field Dispatch I public static final field Heartbeat I public static final field HeartbeatACK I @@ -4277,6 +4287,92 @@ public abstract class love/forte/simbot/qguild/event/Signal { public static final synthetic fun write$Self (Llove/forte/simbot/qguild/event/Signal;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;Lkotlinx/serialization/KSerializer;)V } +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify : love/forte/simbot/qguild/event/Signal { + public static final field Companion Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Companion; + public fun (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data;)V + public final fun component1 ()Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data; + public final fun copy (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify; + public static synthetic fun copy$default (Llove/forte/simbot/qguild/event/Signal$CallbackVerify;Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data;ILjava/lang/Object;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify; + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getData ()Ljava/lang/Object; + public fun getData ()Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public synthetic class love/forte/simbot/qguild/event/Signal$CallbackVerify$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Llove/forte/simbot/qguild/event/Signal$CallbackVerify$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Llove/forte/simbot/qguild/event/Signal$CallbackVerify;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify$Data { + public static final field Companion Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data; + public static synthetic fun copy$default (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data; + public fun equals (Ljava/lang/Object;)Z + public final fun getEventTs ()Ljava/lang/String; + public final fun getPlainToken ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public synthetic class love/forte/simbot/qguild/event/Signal$CallbackVerify$Data$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Data;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify$Data$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify$Verified { + public static final field Companion Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified; + public static synthetic fun copy$default (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified; + public fun equals (Ljava/lang/Object;)Z + public final fun getPlainToken ()Ljava/lang/String; + public final fun getSignature ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public synthetic class love/forte/simbot/qguild/event/Signal$CallbackVerify$Verified$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class love/forte/simbot/qguild/event/Signal$CallbackVerify$Verified$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class love/forte/simbot/qguild/event/Signal$Companion { public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer; } @@ -4284,6 +4380,7 @@ public final class love/forte/simbot/qguild/event/Signal$Companion { public abstract class love/forte/simbot/qguild/event/Signal$Dispatch : love/forte/simbot/qguild/event/Signal { public static final field Companion Llove/forte/simbot/qguild/event/Signal$Dispatch$Companion; public static final field DISPATCH_CLASS_DISCRIMINATOR Ljava/lang/String; + public fun ()V public synthetic fun (ILlove/forte/simbot/qguild/event/Opcode;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public abstract fun getData ()Ljava/lang/Object; public abstract fun getId ()Ljava/lang/String; @@ -4526,8 +4623,14 @@ public final class love/forte/simbot/qguild/event/Signal$Resume$Data$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class love/forte/simbot/qguild/event/SignalDispatchResolvers_generatedKt { + public static final fun resolveDispatchSerializer (Ljava/lang/String;)Lkotlinx/serialization/KSerializer; +} + public final class love/forte/simbot/qguild/event/SignalKt { public static final fun getOpcode (Lkotlinx/serialization/json/JsonElement;)Ljava/lang/Integer; + public static final fun resolveDispatchSerializer (Lkotlinx/serialization/json/JsonObject;Z)Lkotlinx/serialization/KSerializer; + public static synthetic fun resolveDispatchSerializer$default (Lkotlinx/serialization/json/JsonObject;ZILjava/lang/Object;)Lkotlinx/serialization/KSerializer; } public final class love/forte/simbot/qguild/message/ArkBuilder { diff --git a/simbot-component-qq-guild-api/build.gradle.kts b/simbot-component-qq-guild-api/build.gradle.kts index 08407864..eb06b6f0 100644 --- a/simbot-component-qq-guild-api/build.gradle.kts +++ b/simbot-component-qq-guild-api/build.gradle.kts @@ -75,7 +75,7 @@ kotlin { api(libs.simbot.common.apidefinition) api(libs.simbot.common.suspend) api(libs.simbot.common.core) - compileOnly(libs.simbot.common.annotations) + implementation(libs.simbot.common.annotations) api(libs.ktor.client.core) api(libs.ktor.client.contentNegotiation) @@ -90,26 +90,15 @@ kotlin { implementation(libs.ktor.client.mock) } - jvmMain.dependencies { -// compileOnly(libs.simbot.api) // use @Api4J annotation - } - jvmTest.dependencies { implementation(libs.ktor.client.cio) implementation(libs.log4j.api) implementation(libs.log4j.core) implementation(libs.log4j.slf4j2) -// implementation(libs.kotlinx.coroutines.reactor) -// implementation(libs.reactor.core) } jsMain.dependencies { api(libs.ktor.client.js) - implementation(libs.simbot.common.annotations) - } - - nativeMain.dependencies { - implementation(libs.simbot.common.annotations) } mingwTest.dependencies { @@ -122,6 +111,7 @@ kotlin { dependencies { add("kspJvm", project(":internal-processors:api-reader")) add("kspCommonMainMetadata", project(":internal-processors:intents-processor")) + add("kspCommonMainMetadata", project(":internal-processors:dispatch-serializer-processor")) } ksp { diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/OptAnnotations.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/OptAnnotations.kt index 05492542..eb284b8c 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/OptAnnotations.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/OptAnnotations.kt @@ -43,3 +43,10 @@ public annotation class QGApi4JS @MustBeDocumented @RequiresOptIn("Internal API", level = RequiresOptIn.Level.WARNING) public annotation class QGInternalApi + +/** + * A auto-generated API. + */ +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +public annotation class Generated diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/message/MessageSendApi.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/message/MessageSendApi.kt index c78549c6..44134188 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/message/MessageSendApi.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/message/MessageSendApi.kt @@ -171,6 +171,7 @@ public class MessageSendApi private constructor( * */ @Serializable + @ConsistentCopyVisibility public data class Body internal constructor( /** * 选填,消息内容,文本内容,支持[内嵌格式](https://bot.q.qq.com/wiki/develop/api/openapi/message/message_format.html) diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/EventIntents.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/EventIntents.kt index 276c6fe2..0787d2b1 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/EventIntents.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/EventIntents.kt @@ -622,9 +622,10 @@ public val EventIntentsInstances: Array */ @Serializable @SerialName(READY_TYPE) +@DispatchTypeName(READY_TYPE) public data class Ready( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Data ) : Signal.Dispatch() { /** @@ -647,9 +648,10 @@ public data class Ready( */ @Serializable @SerialName(RESUMED_TYPE) +@DispatchTypeName(RESUMED_TYPE) public data class Resumed( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: String ) : Signal.Dispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Opcode.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Opcode.kt index 02d9acec..c5b0854f 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Opcode.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Opcode.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023. ForteScarlet. + * Copyright (c) 2022-2024. ForteScarlet. * * This file is part of simbot-component-qq-guild. * @@ -17,9 +17,13 @@ package love.forte.simbot.qguild.event -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder /** @@ -40,6 +44,7 @@ public sealed interface SendAble * [`opcode`](https://bot.q.qq.com/wiki/develop/api/gateway/opcode.html) 常量类。 * */ +@Suppress("ConstPropertyName") public object Opcodes { /** 服务端进行消息推送 */ public const val Dispatch: Int = 0 @@ -63,6 +68,13 @@ public object Opcodes { /** 当发送心跳成功之后,就会收到该消息 */ public const val HeartbeatACK: Int = 11 + + /** + * 回调地址验证。开放平台对机器人服务端进行验证 + * + * 参考 [官方webhook文档](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/event-emit.html) + */ + public const val CallbackVerify: Int = 13 } /** @@ -101,6 +113,14 @@ public sealed class Opcode(public val code: Int) { // 11 public object HeartbeatACK : Opcode(Opcodes.HeartbeatACK), ReceiveAble + /** + * Callback地址验证 + * + * 参考 [官方webhook文档](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/event-emit.html) + * + * CODE: 13 + */ + public object CallbackVerify : Opcode(Opcodes.CallbackVerify), ReceiveAble public object SerializerByCode : KSerializer { override fun deserialize(decoder: Decoder): Opcode { @@ -113,6 +133,7 @@ public sealed class Opcode(public val code: Int) { Opcodes.InvalidSession -> InvalidSession Opcodes.Hello -> Hello Opcodes.HeartbeatACK -> HeartbeatACK + Opcodes.CallbackVerify -> CallbackVerify else -> throw NoSuchElementException("opcode: $code") } } diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Signal.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Signal.kt index b2922a1c..c7d7ebef 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Signal.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/Signal.kt @@ -152,13 +152,20 @@ public sealed class Signal(@Serializable(Opcode.SerializerByCode::class) publ * * 当一个事件的解析出现异常或存在未知的 `type` 时,可被解析为 [Unknown] 并仅携带原始信息。 * + * ## 序列化器 + * 建议使用 [resolveDispatchSerializer] 通过 JSON 中 + * [Dispatch.DISPATCH_CLASS_DISCRIMINATOR] 的值来获取对应的序列化器, + * 而不是使用多态。 + * + * [Dispatch] 从 `v4.1.0` 开始不再是 `sealed` ,不能直接使用多态序列化。 + * * @see love.forte.simbot.qguild.event * */ @OptIn(ExperimentalSerializationApi::class) @Serializable @JsonClassDiscriminator(Dispatch.DISPATCH_CLASS_DISCRIMINATOR) - public sealed class Dispatch : Signal<@Contextual Any>(Opcode.Dispatch) { + public abstract class Dispatch : Signal<@Contextual Any>(Opcode.Dispatch) { /** * 事件序列 @@ -180,6 +187,9 @@ public sealed class Signal(@Serializable(Opcode.SerializerByCode::class) publ public companion object { + + internal const val DEFAULT_SEQ: Long = -1L + /** * [Dispatch] 使用 [Json] 进行多态解析时的类鉴别器属性名。 * @@ -215,12 +225,56 @@ public sealed class Signal(@Serializable(Opcode.SerializerByCode::class) publ */ public data class Unknown @QGInternalApi constructor( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, override val data: JsonElement, val raw: String ) : Dispatch() } + + /** + * 回调地址验证。开放平台对机器人服务端进行验证 + * + * @see Opcode.CallbackVerify + */ + @Serializable + public data class CallbackVerify( + @SerialName("d") override val data: Data + ) : Signal(Opcode.CallbackVerify) { + + + /** + * 请求结构(`Payload.d`) + * + * | 字段 | 描述 | + * | --- | --- | + * | plain_token | 需要计算签名的字符串 | + * | event_ts | 计算签名使用时间戳 | + */ + @Serializable + public data class Data( + @SerialName("plain_token") + val plainToken: String, + @SerialName("event_ts") + val eventTs: String + ) + + /** + * 回调地址验证返回结果 + * + * | 字段 | 描述 | + * | --- | --- | + * | plain_token | 需要计算签名的字符串 | + * | signature | 签名 | + * + */ + @Serializable + public data class Verified( + @SerialName("plain_token") + val plainToken: String, + val signature: String, + ) + } } @@ -230,6 +284,16 @@ public sealed class Signal(@Serializable(Opcode.SerializerByCode::class) publ */ public fun JsonElement.getOpcode(): Int? = jsonObject["op"]?.jsonPrimitive?.int +@QGInternalApi +public fun JsonElement.tryGetId(): String? = kotlin.runCatching { + jsonObject["id"]?.jsonPrimitive?.contentOrNull +}.getOrNull() + +@QGInternalApi +public fun JsonElement.tryGetSeq(): Long? = kotlin.runCatching { + jsonObject["s"]?.jsonPrimitive?.longOrNull +}.getOrNull() + /** * [Shared](https://bot.q.qq.com/wiki/develop/api/gateway/shard.html) * @@ -279,3 +343,30 @@ internal object SharedSerializer : KSerializer { arraySerializer.serialize(encoder, intArrayOf(value.value, value.total)) } } + +/** + * @suppress Used by symbol processor. + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +public annotation class DispatchTypeName(val value: String) + +/** + * 解析 [json] 并寻找匹配的 [KSerializer], + * 如果找不到则得到 `null`。 + * + * @see resolveDispatchSerializer + */ +public fun resolveDispatchSerializer( + json: JsonObject, + allowNameMissing: Boolean = false, +): KSerializer? { + val eventName = json[Signal.Dispatch.DISPATCH_CLASS_DISCRIMINATOR]?.jsonPrimitive?.contentOrNull + require(allowNameMissing || eventName != null) { + "Required json attribute $.${Signal.Dispatch.DISPATCH_CLASS_DISCRIMINATOR} is missing" + } + + eventName ?: return null + + return resolveDispatchSerializer(eventName) +} diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cManagements.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cManagements.kt index 16353f19..dfaafe16 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cManagements.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cManagements.kt @@ -47,9 +47,10 @@ public sealed class C2CManagementDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.FRIEND_ADD_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.FRIEND_ADD_TYPE) public data class FriendAdd( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: C2CManagementData ) : C2CManagementDispatch() @@ -61,9 +62,10 @@ public data class FriendAdd( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.FRIEND_DEL_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.FRIEND_DEL_TYPE) public data class FriendDel( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: C2CManagementData ) : C2CManagementDispatch() @@ -75,9 +77,10 @@ public data class FriendDel( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.C2C_MSG_REJECT_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.C2C_MSG_REJECT_TYPE) public data class C2CMsgReject( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: C2CManagementData ) : C2CManagementDispatch() @@ -89,9 +92,10 @@ public data class C2CMsgReject( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.C2C_MSG_RECEIVE_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.C2C_MSG_RECEIVE_TYPE) public data class C2CMsgReceive( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: C2CManagementData ) : C2CManagementDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cMessages.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cMessages.kt index fff4af9a..e131a143 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cMessages.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/c2cMessages.kt @@ -28,9 +28,10 @@ import love.forte.simbot.qguild.model.Message */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.C2C_MESSAGE_CREATE_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.C2C_MESSAGE_CREATE_TYPE) public data class C2CMessageCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Data, ) : Signal.Dispatch() { @@ -72,9 +73,10 @@ public data class C2CMessageCreate( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.GROUP_AT_MESSAGE_CREATE_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.GROUP_AT_MESSAGE_CREATE_TYPE) public data class GroupAtMessageCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Data, ) : Signal.Dispatch() { diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/channels.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/channels.kt index 8a1894e1..35b9d307 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/channels.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/channels.kt @@ -43,9 +43,10 @@ public sealed class ChannelDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.Guilds.CHANNEL_CREATE_TYPE) +@DispatchTypeName(EventIntents.Guilds.CHANNEL_CREATE_TYPE) public data class ChannelCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventChannel ) : ChannelDispatch() @@ -57,9 +58,10 @@ public data class ChannelCreate( */ @Serializable @SerialName(EventIntents.Guilds.CHANNEL_UPDATE_TYPE) +@DispatchTypeName(EventIntents.Guilds.CHANNEL_UPDATE_TYPE) public data class ChannelUpdate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventChannel ) : ChannelDispatch() @@ -71,9 +73,10 @@ public data class ChannelUpdate( */ @Serializable @SerialName(EventIntents.Guilds.CHANNEL_DELETE_TYPE) +@DispatchTypeName(EventIntents.Guilds.CHANNEL_DELETE_TYPE) public data class ChannelDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventChannel ) : ChannelDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/forums.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/forums.kt index b3bda947..b5c22628 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/forums.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/forums.kt @@ -92,9 +92,10 @@ public sealed class ForumThreadDispatch : ForumDispatch() { */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_THREAD_CREATE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_THREAD_CREATE_TYPE) public data class ForumThreadCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Thread ) : ForumThreadDispatch() @@ -106,9 +107,10 @@ public data class ForumThreadCreate( */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_THREAD_UPDATE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_THREAD_UPDATE_TYPE) public data class ForumThreadUpdate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Thread ) : ForumThreadDispatch() @@ -120,9 +122,10 @@ public data class ForumThreadUpdate( */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_THREAD_DELETE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_THREAD_DELETE_TYPE) public data class ForumThreadDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Thread ) : ForumThreadDispatch() @@ -147,9 +150,10 @@ public sealed class ForumPostDispatch : ForumDispatch() { */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_POST_CREATE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_POST_CREATE_TYPE) public data class ForumPostCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Post ) : ForumPostDispatch() @@ -161,9 +165,10 @@ public data class ForumPostCreate( */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_POST_DELETE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_POST_DELETE_TYPE) public data class ForumPostDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Post ) : ForumPostDispatch() @@ -188,9 +193,10 @@ public sealed class ForumReplyDispatch : ForumDispatch() { */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_REPLY_CREATE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_REPLY_CREATE_TYPE) public data class ForumReplyCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Reply ) : ForumReplyDispatch() @@ -202,9 +208,10 @@ public data class ForumReplyCreate( */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_REPLY_DELETE_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_REPLY_DELETE_TYPE) public data class ForumReplyDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Reply ) : ForumReplyDispatch() @@ -215,8 +222,9 @@ public data class ForumReplyDelete( */ @Serializable @SerialName(EventIntents.ForumsEvent.FORUM_PUBLISH_AUDIT_RESULT_TYPE) +@DispatchTypeName(EventIntents.ForumsEvent.FORUM_PUBLISH_AUDIT_RESULT_TYPE) public data class ForumPublishAuditResult( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: AuditResult ) : ForumDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/groupManagements.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/groupManagements.kt index 54ee95e7..3eb5cc41 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/groupManagements.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/groupManagements.kt @@ -49,9 +49,10 @@ public sealed class GroupRobotManagementDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.GROUP_ADD_ROBOT_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.GROUP_ADD_ROBOT_TYPE) public data class GroupAddRobot( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: GroupRobotManagementData ) : GroupRobotManagementDispatch() @@ -63,9 +64,10 @@ public data class GroupAddRobot( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.GROUP_DEL_ROBOT_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.GROUP_DEL_ROBOT_TYPE) public data class GroupDelRobot( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: GroupRobotManagementData ) : GroupRobotManagementDispatch() @@ -77,9 +79,10 @@ public data class GroupDelRobot( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.GROUP_MSG_REJECT_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.GROUP_MSG_REJECT_TYPE) public data class GroupMsgReject( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: GroupRobotManagementData ) : GroupRobotManagementDispatch() @@ -91,9 +94,10 @@ public data class GroupMsgReject( */ @Serializable @SerialName(EventIntents.GroupAndC2CEvent.GROUP_MSG_RECEIVE_TYPE) +@DispatchTypeName(EventIntents.GroupAndC2CEvent.GROUP_MSG_RECEIVE_TYPE) public data class GroupMsgReceive( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: GroupRobotManagementData ) : GroupRobotManagementDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/guilds.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/guilds.kt index 3caeb9ee..20f8854e 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/guilds.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/guilds.kt @@ -41,9 +41,10 @@ public sealed class EventGuildDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.Guilds.GUILD_CREATE_TYPE) +@DispatchTypeName(EventIntents.Guilds.GUILD_CREATE_TYPE) public data class GuildCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventGuild ) : EventGuildDispatch() @@ -57,9 +58,10 @@ public data class GuildCreate( */ @Serializable @SerialName(EventIntents.Guilds.GUILD_UPDATE_TYPE) +@DispatchTypeName(EventIntents.Guilds.GUILD_UPDATE_TYPE) public data class GuildUpdate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventGuild ) : EventGuildDispatch() @@ -74,9 +76,10 @@ public data class GuildUpdate( */ @Serializable @SerialName(EventIntents.Guilds.GUILD_DELETE_TYPE) +@DispatchTypeName(EventIntents.Guilds.GUILD_DELETE_TYPE) public data class GuildDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventGuild ) : EventGuildDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/members.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/members.kt index 4114a455..5616a1d3 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/members.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/members.kt @@ -31,9 +31,10 @@ import love.forte.simbot.qguild.time.ZERO_ISO_INSTANT */ @Serializable @SerialName(EventIntents.GuildMembers.GUILD_MEMBER_ADD_TYPE) +@DispatchTypeName(EventIntents.GuildMembers.GUILD_MEMBER_ADD_TYPE) public data class GuildMemberAdd( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventMember ) : Signal.Dispatch() @@ -44,9 +45,10 @@ public data class GuildMemberAdd( */ @Serializable @SerialName(EventIntents.GuildMembers.GUILD_MEMBER_UPDATE_TYPE) +@DispatchTypeName(EventIntents.GuildMembers.GUILD_MEMBER_UPDATE_TYPE) public data class GuildMemberUpdate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventMember ) : Signal.Dispatch() @@ -57,9 +59,10 @@ public data class GuildMemberUpdate( */ @Serializable @SerialName(EventIntents.GuildMembers.GUILD_MEMBER_REMOVE_TYPE) +@DispatchTypeName(EventIntents.GuildMembers.GUILD_MEMBER_REMOVE_TYPE) public data class GuildMemberRemove( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: EventMember ) : Signal.Dispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/messages.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/messages.kt index 38888677..4d50d083 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/messages.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/messages.kt @@ -44,9 +44,10 @@ public sealed class MessageDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.PublicGuildMessages.AT_MESSAGE_CREATE_TYPE) +@DispatchTypeName(EventIntents.PublicGuildMessages.AT_MESSAGE_CREATE_TYPE) public data class AtMessageCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Message ) : MessageDispatch() @@ -57,9 +58,10 @@ public data class AtMessageCreate( */ @Serializable @SerialName(EventIntents.PublicGuildMessages.PUBLIC_MESSAGE_DELETE_TYPE) +@DispatchTypeName(EventIntents.PublicGuildMessages.PUBLIC_MESSAGE_DELETE_TYPE) public data class PublicMessageDeleteCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Unit /* TODO 文档没找到描述。 */ ) : Signal.Dispatch() @@ -74,9 +76,10 @@ public data class PublicMessageDeleteCreate( */ @Serializable @SerialName(EventIntents.DirectMessage.DIRECT_MESSAGE_CREATE_TYPE) +@DispatchTypeName(EventIntents.DirectMessage.DIRECT_MESSAGE_CREATE_TYPE) public data class DirectMessageCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Message ) : MessageDispatch() @@ -95,9 +98,10 @@ public sealed class MessageAuditedDispatch : Signal.Dispatch() { */ @Serializable @SerialName(EventIntents.GuildMessages.MESSAGE_CREATE_TYPE) +@DispatchTypeName(EventIntents.GuildMessages.MESSAGE_CREATE_TYPE) public data class MessageCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Message ) : MessageDispatch() @@ -106,9 +110,10 @@ public data class MessageCreate( */ @Serializable @SerialName(EventIntents.GuildMessages.MESSAGE_DELETE_TYPE) +@DispatchTypeName(EventIntents.GuildMessages.MESSAGE_DELETE_TYPE) public data class MessageDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: Unit /* TODO 文档没找到描述。 */ ) : Signal.Dispatch() @@ -123,9 +128,10 @@ public data class MessageDelete( */ @Serializable @SerialName(EventIntents.MessageAudit.MESSAGE_AUDIT_PASS_TYPE) +@DispatchTypeName(EventIntents.MessageAudit.MESSAGE_AUDIT_PASS_TYPE) public data class MessageAuditPass( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: MessageAudited ) : MessageAuditedDispatch() @@ -139,8 +145,9 @@ public data class MessageAuditPass( */ @Serializable @SerialName(EventIntents.MessageAudit.MESSAGE_AUDIT_REJECT_TYPE) +@DispatchTypeName(EventIntents.MessageAudit.MESSAGE_AUDIT_REJECT_TYPE) public data class MessageAuditReject( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: MessageAudited ) : MessageAuditedDispatch() diff --git a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/openForums.kt b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/openForums.kt index 7ddd3527..63738ffe 100644 --- a/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/openForums.kt +++ b/simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/event/openForums.kt @@ -118,9 +118,10 @@ public sealed class OpenForumThreadDispatch : OpenForumDispatch() { */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_CREATE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_CREATE_TYPE) public data class OpenForumThreadCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumThreadData ) : OpenForumThreadDispatch() @@ -132,9 +133,10 @@ public data class OpenForumThreadCreate( */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_UPDATE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_UPDATE_TYPE) public data class OpenForumThreadUpdate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumThreadData ) : OpenForumThreadDispatch() @@ -146,9 +148,10 @@ public data class OpenForumThreadUpdate( */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_DELETE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_THREAD_DELETE_TYPE) public data class OpenForumThreadDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumThreadData ) : OpenForumThreadDispatch() @@ -186,9 +189,10 @@ public sealed class OpenForumPostDispatch : OpenForumDispatch() { */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_POST_CREATE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_POST_CREATE_TYPE) public data class OpenForumPostCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumPostData ) : OpenForumPostDispatch() @@ -200,9 +204,10 @@ public data class OpenForumPostCreate( */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_POST_DELETE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_POST_DELETE_TYPE) public data class OpenForumPostDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumPostData ) : OpenForumPostDispatch() @@ -239,9 +244,10 @@ public sealed class OpenForumReplyDispatch : OpenForumDispatch() { */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_REPLY_CREATE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_REPLY_CREATE_TYPE) public data class OpenForumReplyCreate( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumReplyData ) : OpenForumReplyDispatch() @@ -253,9 +259,10 @@ public data class OpenForumReplyCreate( */ @Serializable @SerialName(EventIntents.OpenForumsEvent.OPEN_FORUM_REPLY_DELETE_TYPE) +@DispatchTypeName(EventIntents.OpenForumsEvent.OPEN_FORUM_REPLY_DELETE_TYPE) public data class OpenForumReplyDelete( override val id: String? = null, - override val s: Long, + override val s: Long = DEFAULT_SEQ, @SerialName("d") override val data: OpenForumReplyData ) : OpenForumReplyDispatch() diff --git a/simbot-component-qq-guild-core/api/simbot-component-qq-guild-core.api b/simbot-component-qq-guild-core/api/simbot-component-qq-guild-core.api index 67bb20df..bf1c24cd 100644 --- a/simbot-component-qq-guild-core/api/simbot-component-qq-guild-core.api +++ b/simbot-component-qq-guild-core/api/simbot-component-qq-guild-core.api @@ -73,6 +73,20 @@ public abstract interface class love/forte/simbot/component/qguild/QQGuildUsageB } public abstract interface class love/forte/simbot/component/qguild/bot/QGBot : love/forte/simbot/ability/EventMentionAware, love/forte/simbot/bot/Bot { + public synthetic fun emitEvent (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public synthetic fun emitEvent (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun emitEvent$default (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun emitEvent$suspendImpl (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun emitEvent$suspendImpl (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun emitEventAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; + public fun emitEventAsync (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Ljava/util/concurrent/CompletableFuture; + public static synthetic fun emitEventAsync$default (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Ljava/util/concurrent/CompletableFuture; + public fun emitEventBlocking (Ljava/lang/String;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public fun emitEventBlocking (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public static synthetic fun emitEventBlocking$default (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public fun emitEventReserve (Ljava/lang/String;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; + public fun emitEventReserve (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; + public static synthetic fun emitEventReserve$default (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; public abstract fun getAvatar ()Ljava/lang/String; public abstract fun getComponent ()Llove/forte/simbot/component/qguild/QQGuildComponent; public fun getContactRelation ()Llove/forte/simbot/bot/ContactRelation; @@ -135,12 +149,19 @@ public abstract interface class love/forte/simbot/component/qguild/bot/QGBot : l public fun uploadUserMediaReserve (Llove/forte/simbot/common/id/ID;Ljava/lang/String;I)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; } +public final class love/forte/simbot/component/qguild/bot/QGBotKt { + public static final fun emitEvent (Llove/forte/simbot/component/qguild/bot/QGBot;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface annotation class love/forte/simbot/component/qguild/bot/QGBotManagerConfigurationDsl : java/lang/annotation/Annotation { } public abstract interface class love/forte/simbot/component/qguild/bot/QQGuildBotManager : love/forte/simbot/bot/BotManager { public static final field Factory Llove/forte/simbot/component/qguild/bot/QQGuildBotManager$Factory; + public abstract fun all ()Lkotlin/sequences/Sequence; + public abstract fun all (Llove/forte/simbot/common/id/ID;)Lkotlin/sequences/Sequence; public fun configurable (Llove/forte/simbot/bot/SerializableBotConfiguration;)Z + public abstract fun get (Llove/forte/simbot/common/id/ID;)Llove/forte/simbot/component/qguild/bot/QGBot; public abstract fun getConfiguration ()Llove/forte/simbot/component/qguild/bot/QQGuildBotManagerConfiguration; public abstract fun getEventDispatcher ()Llove/forte/simbot/event/EventDispatcher; public fun register (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Llove/forte/simbot/component/qguild/bot/QGBot; @@ -412,12 +433,14 @@ public final class love/forte/simbot/component/qguild/bot/config/QGBotFileConfig public fun equals (Ljava/lang/Object;)Z public final fun getCacheConfig ()Llove/forte/simbot/component/qguild/bot/config/CacheConfig; public final fun getClientProperties ()Ljava/util/Map; + public final fun getDisableWs ()Ljava/lang/Boolean; public final fun getDispatcherConfiguration ()Llove/forte/simbot/bot/configuration/DispatcherConfiguration; public final fun getIntentsConfig ()Llove/forte/simbot/component/qguild/bot/config/IntentsConfig; public final fun getServerUrl ()Ljava/lang/String; public final fun getShardConfig ()Llove/forte/simbot/component/qguild/bot/config/ShardConfig; public final fun getTimeoutConfig ()Llove/forte/simbot/component/qguild/bot/config/QGBotFileConfiguration$TimeoutConfig; public fun hashCode ()I + public final fun setDisableWs (Ljava/lang/Boolean;)V public fun toString ()Ljava/lang/String; } @@ -1522,11 +1545,11 @@ public abstract interface class love/forte/simbot/component/qguild/guild/QGMembe public fun getName ()Ljava/lang/String; public fun getNick ()Ljava/lang/String; public fun getRoleIds ()Ljava/util/List; - public abstract fun mute (JLjava/util/concurrent/TimeUnit;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract synthetic fun mute (JLjava/util/concurrent/TimeUnit;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract synthetic fun mute-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun muteAsync (JLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/CompletableFuture; - public abstract fun muteBlocking (JLjava/util/concurrent/TimeUnit;)V - public abstract fun muteReserve (JLjava/util/concurrent/TimeUnit;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; + public fun muteAsync (JLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/CompletableFuture; + public fun muteBlocking (JLjava/util/concurrent/TimeUnit;)V + public fun muteReserve (JLjava/util/concurrent/TimeUnit;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; public abstract synthetic fun send (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract synthetic fun send (Llove/forte/simbot/message/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract synthetic fun send (Llove/forte/simbot/message/MessageContent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -1612,8 +1635,6 @@ public final class love/forte/simbot/component/qguild/message/QGArk : love/forte public static final fun byArk (Llove/forte/simbot/qguild/model/Message$Ark;)Llove/forte/simbot/component/qguild/message/QGArk; public final fun component1 ()Llove/forte/simbot/common/id/ID; public final fun component2 ()Ljava/util/List; - public final fun copy (Llove/forte/simbot/common/id/ID;Ljava/util/List;)Llove/forte/simbot/component/qguild/message/QGArk; - public static synthetic fun copy$default (Llove/forte/simbot/component/qguild/message/QGArk;Llove/forte/simbot/common/id/ID;Ljava/util/List;ILjava/lang/Object;)Llove/forte/simbot/component/qguild/message/QGArk; public static final fun create (Llove/forte/simbot/common/id/ID;Ljava/util/List;)Llove/forte/simbot/component/qguild/message/QGArk; public fun equals (Ljava/lang/Object;)Z public final fun getKvs ()Ljava/util/List; @@ -1766,8 +1787,6 @@ public final class love/forte/simbot/component/qguild/message/QGEmbed : love/for public static final field Companion Llove/forte/simbot/component/qguild/message/QGEmbed$Companion; public static final fun byEmbed (Llove/forte/simbot/qguild/model/Message$Embed;)Llove/forte/simbot/component/qguild/message/QGEmbed; public final fun component1 ()Llove/forte/simbot/qguild/model/Message$Embed; - public final fun copy (Llove/forte/simbot/qguild/model/Message$Embed;)Llove/forte/simbot/component/qguild/message/QGEmbed; - public static synthetic fun copy$default (Llove/forte/simbot/component/qguild/message/QGEmbed;Llove/forte/simbot/qguild/model/Message$Embed;ILjava/lang/Object;)Llove/forte/simbot/component/qguild/message/QGEmbed; public fun equals (Ljava/lang/Object;)Z public final fun getEmbed ()Llove/forte/simbot/qguild/model/Message$Embed; public fun hashCode ()I @@ -1806,8 +1825,6 @@ public final class love/forte/simbot/component/qguild/message/QGMarkdown : love/ public static final field Companion Llove/forte/simbot/component/qguild/message/QGMarkdown$Companion; public static final fun byMarkdown (Llove/forte/simbot/qguild/model/Message$Markdown;)Llove/forte/simbot/component/qguild/message/QGMarkdown; public final fun component1 ()Llove/forte/simbot/qguild/model/Message$Markdown; - public final fun copy (Llove/forte/simbot/qguild/model/Message$Markdown;)Llove/forte/simbot/component/qguild/message/QGMarkdown; - public static synthetic fun copy$default (Llove/forte/simbot/component/qguild/message/QGMarkdown;Llove/forte/simbot/qguild/model/Message$Markdown;ILjava/lang/Object;)Llove/forte/simbot/component/qguild/message/QGMarkdown; public static final fun create (Ljava/lang/String;)Llove/forte/simbot/component/qguild/message/QGMarkdown; public static final fun createByCustomTemplateId (Ljava/lang/String;)Llove/forte/simbot/component/qguild/message/QGMarkdown; public static final fun createByCustomTemplateId (Ljava/lang/String;Llove/forte/simbot/qguild/model/Message$Markdown$Params;)Llove/forte/simbot/component/qguild/message/QGMarkdown; diff --git a/simbot-component-qq-guild-core/build.gradle.kts b/simbot-component-qq-guild-core/build.gradle.kts index aa34e1e9..e08a8a12 100644 --- a/simbot-component-qq-guild-core/build.gradle.kts +++ b/simbot-component-qq-guild-core/build.gradle.kts @@ -52,14 +52,17 @@ kotlin { } applyTier1() - applyTier2() + applyTier2( + // stdlib 不再支持 + watchosX64 = false + ) applyTier3(supportKtorClient = true) sourceSets { commonMain.dependencies { - compileOnly(libs.simbot.api) api(project(":simbot-component-qq-guild-stdlib")) - compileOnly(libs.simbot.common.annotations) + implementation(libs.simbot.api) + implementation(libs.simbot.common.annotations) // ktor api(libs.ktor.client.contentNegotiation) api(libs.ktor.serialization.kotlinxJson) @@ -78,26 +81,13 @@ kotlin { } jvmTest.dependencies { -// implementation(libs.ktor.client.cio) implementation(libs.ktor.client.java) -// runtimeOnly(libs.kotlinx.coroutines.reactor) -// implementation(libs.reactor.core) implementation(libs.log4j.api) implementation(libs.log4j.core) implementation(libs.log4j.slf4j2) } - jsMain.dependencies { - implementation(libs.simbot.api) - api(libs.simbot.common.annotations) - } - - nativeMain.dependencies { - implementation(libs.simbot.api) - api(libs.simbot.common.annotations) - } - mingwTest.dependencies { implementation(libs.ktor.client.winhttp) } diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QGBot.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QGBot.kt index 76986b67..31068112 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QGBot.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QGBot.kt @@ -48,9 +48,7 @@ import love.forte.simbot.qguild.api.QQGuildApi import love.forte.simbot.qguild.api.files.UploadGroupFilesApi import love.forte.simbot.qguild.api.files.UploadUserFilesApi import love.forte.simbot.qguild.api.message.GetMessageApi -import love.forte.simbot.qguild.stdlib.requestBy -import love.forte.simbot.qguild.stdlib.requestDataBy -import love.forte.simbot.qguild.stdlib.requestTextBy +import love.forte.simbot.qguild.stdlib.* import love.forte.simbot.suspendrunner.ST import love.forte.simbot.suspendrunner.STP import kotlin.jvm.JvmSynthetic @@ -385,8 +383,10 @@ public interface QGBot : Bot, EventMentionAware { */ @ST override suspend fun messageFromId(id: ID): QGMessageContent { - throw UnsupportedOperationException("Cannot query message from `messageId` only. " + - "Use the QGBot.messageFromId(channelId, messageId) or QGBot.messageFromReference(QGReference) plz.") + throw UnsupportedOperationException( + "Cannot query message from `messageId` only. " + + "Use the QGBot.messageFromId(channelId, messageId) or QGBot.messageFromReference(QGReference) plz." + ) } /** @@ -401,8 +401,10 @@ public interface QGBot : Bot, EventMentionAware { @ExperimentalQGApi override suspend fun messageFromReference(reference: MessageReference): QGMessageContent { if (reference !is QGReference) { - throw UnsupportedOperationException("Cannot query message use a reference that type is not QGReference. " + - "Use `reference` type of QGReference or use messageFromId(channelId, messageId) plz.") + throw UnsupportedOperationException( + "Cannot query message use a reference that type is not QGReference. " + + "Use `reference` type of QGReference or use messageFromId(channelId, messageId) plz." + ) } val cid = reference.channelId ?: throw IllegalArgumentException("`reference.channelId` must not be null") @@ -422,4 +424,54 @@ public interface QGBot : Bot, EventMentionAware { val data = executeData(api) return QGMessageContentImpl(this, data) } + + + /** + * 主动推送一个事件原文。 + * 可用于在 webhook 模式下推送事件。 + * + * @param payload 接收到的事件推送的JSON格式正文字符串。 + * @param options 额外提供的属性或配置。默认为 `null`。 + * + * @throws IllegalArgumentException 参考: + * - [EmitEventOptions.ignoreUnknownOpcode] + * - [EmitEventOptions.ignoreMissingOpcode] + * + * @see QGSourceBot.emitEvent + * + * @since 4.1.0 + */ + @ST + public suspend fun emitEvent( + payload: String, + options: EmitEventOptions? = null, + ): EmitResult { + return source.emitEvent(payload, options) + } + + /** + * 主动推送一个事件原文。 + * 可用于在 webhook 模式下推送事件。 + * + * @param payload 接收到的事件推送的JSON格式正文字符串。 + * + * @see QGSourceBot.emitEvent + * @since 4.1.0 + */ + @ST + public suspend fun emitEvent(payload: String): EmitResult { + return source.emitEvent(payload) + } +} + +/** + * 使用 [QGBot.emitEvent] 推送一个外部事件,并且在 [block] 中配置 [EmitEventOptions]。 + * @see QGBot.emitEvent + * @since 4.1.0 + */ +public suspend inline fun QGBot.emitEvent( + payload: String, + block: EmitEventOptions.() -> Unit +): EmitResult { + return source.emitEvent(payload, block) } diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QQGuildBotManager.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QQGuildBotManager.kt index 02358d9b..384697b1 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QQGuildBotManager.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/QQGuildBotManager.kt @@ -26,6 +26,7 @@ import love.forte.simbot.bot.UnsupportedBotConfigurationException import love.forte.simbot.common.coroutines.linkTo import love.forte.simbot.common.function.ConfigurerFunction import love.forte.simbot.common.function.invokeBy +import love.forte.simbot.common.id.ID import love.forte.simbot.component.NoSuchComponentException import love.forte.simbot.component.find import love.forte.simbot.component.qguild.QQGuildComponent @@ -58,6 +59,12 @@ public interface QQGuildBotManager : BotManager { public val eventDispatcher: EventDispatcher public val configuration: QQGuildBotManagerConfiguration + override fun get(id: ID): QGBot + + override fun all(): Sequence + + override fun all(id: ID): Sequence + @OptIn(ExperimentalContracts::class) private fun checkConfig(configuration: SerializableBotConfiguration): Boolean { contract { diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/config/QGBotFileConfiguration.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/config/QGBotFileConfiguration.kt index 3840f8fc..e73df2f2 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/config/QGBotFileConfiguration.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/bot/config/QGBotFileConfiguration.kt @@ -18,7 +18,6 @@ package love.forte.simbot.component.qguild.bot.config import io.ktor.http.* -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import love.forte.simbot.bot.SerializableBotConfiguration @@ -107,7 +106,6 @@ public data class QGBotFileConfiguration( * ``` * */ - @OptIn(ExperimentalSerializationApi::class) @Serializable @UsedOnlyForConfigSerialization public data class Config( @@ -241,7 +239,14 @@ public data class QGBotFileConfiguration( * @see DispatcherConfiguration */ @SerialName("dispatcher") public val dispatcherConfiguration: DispatcherConfiguration? = null, + ) { + /** + * 是否禁用 ws + * @since 4.1.0 + */ + public var disableWs: Boolean? = null + public companion object { internal const val SERVER_URL_SANDBOX_VALUE: String = "SANDBOX" private val DEFAULT = QGBotComponentConfiguration() @@ -302,6 +307,7 @@ public data class QGBotFileConfiguration( configuration.coroutineContext += dispatcher } + disableWs?.also { disableWs -> configuration.disableWs = disableWs } } } } diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/internal/bot/QGBotImpl.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/internal/bot/QGBotImpl.kt index 7f3eecae..dcff66ef 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/internal/bot/QGBotImpl.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/internal/bot/QGBotImpl.kt @@ -408,6 +408,7 @@ internal class QGBotImpl( source.start().also { // set everytime. + logger.debug("Initial @me for {}", source.ticket.appId) botSelf = me().also { me -> logger.debug("bot own information: {}", me) } diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGArk.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGArk.kt index abe1b205..b357ee40 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGArk.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGArk.kt @@ -37,6 +37,7 @@ import love.forte.simbot.message.Message as SimbotMessage */ @SerialName("qg.ark") @Serializable +@ConsistentCopyVisibility public data class QGArk internal constructor( @SerialName("template_id") public val templateId: ID, diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGEmbed.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGEmbed.kt index d84f8030..8e7e75d3 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGEmbed.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGEmbed.kt @@ -47,6 +47,7 @@ import kotlin.jvm.JvmStatic */ @SerialName("qg.embed") @Serializable +@ConsistentCopyVisibility public data class QGEmbed internal constructor(public val embed: Message.Embed) : QGMessageElement { public companion object { diff --git a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGMarkdown.kt b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGMarkdown.kt index 3a18f19c..9cd79b7e 100644 --- a/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGMarkdown.kt +++ b/simbot-component-qq-guild-core/src/commonMain/kotlin/love/forte/simbot/component/qguild/message/QGMarkdown.kt @@ -32,6 +32,7 @@ import kotlin.jvm.JvmStatic * @author ForteScarlet */ @Serializable +@ConsistentCopyVisibility public data class QGMarkdown internal constructor( public val markdown: Message.Markdown ) : QGMessageElement { diff --git a/simbot-component-qq-guild-stdlib/api/simbot-component-qq-guild-stdlib.api b/simbot-component-qq-guild-stdlib/api/simbot-component-qq-guild-stdlib.api index fb1475f2..602176e8 100644 --- a/simbot-component-qq-guild-stdlib/api/simbot-component-qq-guild-stdlib.api +++ b/simbot-component-qq-guild-stdlib/api/simbot-component-qq-guild-stdlib.api @@ -2,6 +2,19 @@ public abstract interface class love/forte/simbot/qguild/stdlib/Bot : kotlinx/co public fun asFuture ()Ljava/util/concurrent/CompletableFuture; public fun cancel ()V public abstract fun cancel (Ljava/lang/Throwable;)V + public synthetic fun emitEvent (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract synthetic fun emitEvent (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun emitEvent$default (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun emitEvent$suspendImpl (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun emitEventAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; + public fun emitEventAsync (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Ljava/util/concurrent/CompletableFuture; + public static synthetic fun emitEventAsync$default (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Ljava/util/concurrent/CompletableFuture; + public fun emitEventBlocking (Ljava/lang/String;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public fun emitEventBlocking (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public static synthetic fun emitEventBlocking$default (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Llove/forte/simbot/qguild/stdlib/EmitResult; + public fun emitEventReserve (Ljava/lang/String;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; + public fun emitEventReserve (Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; + public static synthetic fun emitEventReserve$default (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Llove/forte/simbot/qguild/stdlib/EmitEventOptions;ILjava/lang/Object;)Llove/forte/simbot/suspendrunner/reserve/SuspendReserve; public abstract fun getAccessToken ()Ljava/lang/String; public abstract fun getApiClient ()Lio/ktor/client/HttpClient; public abstract fun getApiDecoder ()Lkotlinx/serialization/json/Json; @@ -68,6 +81,7 @@ public abstract interface class love/forte/simbot/qguild/stdlib/BotConfiguration public abstract fun getApiHttpSocketTimeoutMillis ()Ljava/lang/Long; public abstract fun getClientProperties ()Ljava/util/Map; public abstract fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public abstract fun getDisableWs ()Z public abstract fun getExceptionHandler ()Llove/forte/simbot/qguild/stdlib/ExceptionProcessor; public abstract synthetic fun getIntents-DNrqdk0 ()I public fun getIntentsValue ()I @@ -89,6 +103,10 @@ public final class love/forte/simbot/qguild/stdlib/BotFactory { public static synthetic fun create$default (Llove/forte/simbot/qguild/stdlib/Bot$Ticket;Llove/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration;ILjava/lang/Object;)Llove/forte/simbot/qguild/stdlib/Bot; } +public final class love/forte/simbot/qguild/stdlib/BotKt { + public static final fun emitEvent (Llove/forte/simbot/qguild/stdlib/Bot;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class love/forte/simbot/qguild/stdlib/BotRequests { public static final fun botToken (Llove/forte/simbot/qguild/stdlib/Bot;)Ljava/lang/String; public static final fun qqBotToken (Llove/forte/simbot/qguild/stdlib/Bot;)Ljava/lang/String; @@ -134,6 +152,7 @@ public final class love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration public fun getApiHttpSocketTimeoutMillis ()Ljava/lang/Long; public fun getClientProperties ()Ljava/util/Map; public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public fun getDisableWs ()Z public fun getExceptionHandler ()Llove/forte/simbot/qguild/stdlib/ExceptionProcessor; public synthetic fun getIntents-DNrqdk0 ()I public fun getIntentsValue ()I @@ -149,6 +168,7 @@ public final class love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration public fun setApiHttpSocketTimeoutMillis (Ljava/lang/Long;)V public fun setClientProperties (Ljava/util/Map;)V public fun setCoroutineContext (Lkotlin/coroutines/CoroutineContext;)V + public fun setDisableWs (Z)V public fun setExceptionHandler (Llove/forte/simbot/qguild/stdlib/ExceptionProcessor;)V public synthetic fun setIntents-NLurJl8 (I)V public fun setIntentsValue (I)V @@ -166,6 +186,57 @@ public abstract interface class love/forte/simbot/qguild/stdlib/DisposableHandle public abstract fun dispose ()V } +public final class love/forte/simbot/qguild/stdlib/Ed25519SignatureVerification { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Llove/forte/simbot/qguild/stdlib/Ed25519SignatureVerification; + public static synthetic fun copy$default (Llove/forte/simbot/qguild/stdlib/Ed25519SignatureVerification;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Llove/forte/simbot/qguild/stdlib/Ed25519SignatureVerification; + public fun equals (Ljava/lang/Object;)Z + public final fun getSignatureEd25519 ()Ljava/lang/String; + public final fun getSignatureTimestamp ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class love/forte/simbot/qguild/stdlib/EmitEventOptions { + public fun ()V + public final fun getEd25519SignatureVerification ()Llove/forte/simbot/qguild/stdlib/Ed25519SignatureVerification; + public final fun getIgnoreMissingOpcode ()Z + public final fun getIgnoreUnknownOpcode ()Z + public final fun setEd25519SignatureVerification (Llove/forte/simbot/qguild/stdlib/Ed25519SignatureVerification;)V + public final fun setIgnoreMissingOpcode (Z)V + public final fun setIgnoreUnknownOpcode (Z)V +} + +public abstract class love/forte/simbot/qguild/stdlib/EmitResult { +} + +public final class love/forte/simbot/qguild/stdlib/EmitResult$Dispatched : love/forte/simbot/qguild/stdlib/EmitResult { + public static final field INSTANCE Llove/forte/simbot/qguild/stdlib/EmitResult$Dispatched; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class love/forte/simbot/qguild/stdlib/EmitResult$Nothing : love/forte/simbot/qguild/stdlib/EmitResult { + public static final field INSTANCE Llove/forte/simbot/qguild/stdlib/EmitResult$Nothing; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class love/forte/simbot/qguild/stdlib/EmitResult$Verified : love/forte/simbot/qguild/stdlib/EmitResult { + public fun (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified;)V + public final fun component1 ()Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified; + public final fun copy (Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified;)Llove/forte/simbot/qguild/stdlib/EmitResult$Verified; + public static synthetic fun copy$default (Llove/forte/simbot/qguild/stdlib/EmitResult$Verified;Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified;ILjava/lang/Object;)Llove/forte/simbot/qguild/stdlib/EmitResult$Verified; + public fun equals (Ljava/lang/Object;)Z + public final fun getVerified ()Llove/forte/simbot/qguild/event/Signal$CallbackVerify$Verified; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class love/forte/simbot/qguild/stdlib/EventProcessor { public abstract synthetic fun invoke (Llove/forte/simbot/qguild/event/Signal$Dispatch;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/simbot-component-qq-guild-stdlib/build.gradle.kts b/simbot-component-qq-guild-stdlib/build.gradle.kts index cfd790c6..f014426a 100644 --- a/simbot-component-qq-guild-stdlib/build.gradle.kts +++ b/simbot-component-qq-guild-stdlib/build.gradle.kts @@ -52,7 +52,10 @@ kotlin { } applyTier1() - applyTier2() + applyTier2( + // multiplatform-crypto-libsodium 不支持 watchosX64 target. + watchosX64 = false + ) applyTier3(supportKtorClient = true) sourceSets { @@ -61,36 +64,45 @@ kotlin { api(libs.simbot.common.loop) api(libs.simbot.common.atomic) api(libs.simbot.common.core) - compileOnly(libs.simbot.common.annotations) + implementation(libs.simbot.common.annotations) // ktor api(libs.ktor.client.contentNegotiation) api(libs.ktor.serialization.kotlinxJson) api(libs.ktor.client.ws) + + // https://github.com/andreypfau/curve25519-kotlin +// implementation("io.github.andreypfau:curve25519-kotlin:0.0.8") + + // https://github.com/ionspin/kotlin-multiplatform-libsodium + implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2") } commonTest.dependencies { implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) implementation(libs.ktor.client.mock) + // https://github.com/diglol/crypto +// implementation("com.diglol.crypto:pkc:0.2.0") } jvmTest.dependencies { -// implementation(libs.ktor.client.cio) implementation(libs.ktor.client.java) implementation(libs.log4j.api) implementation(libs.log4j.core) implementation(libs.log4j.slf4j2) + +// implementation("dev.whyoleg.cryptography:cryptography-core:0.4.0") +// implementation(kotlincrypto.core.digest) +// implementation(kotlincrypto.core.mac) +// implementation(kotlincrypto.core.xof) +// implementation(kotlincrypto.macs.hmac.sha1) +// implementation(kotlincrypto.macs.kmac) } jsMain.dependencies { - api(libs.simbot.common.annotations) api(libs.ktor.client.js) } - nativeMain.dependencies { - api(libs.simbot.common.annotations) - } - mingwTest.dependencies { implementation(libs.ktor.client.winhttp) } diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/Bot.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/Bot.kt index 6b3ebecf..a9484199 100644 --- a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/Bot.kt +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/Bot.kt @@ -26,6 +26,7 @@ import kotlinx.serialization.json.Json import love.forte.simbot.qguild.api.GatewayInfo import love.forte.simbot.qguild.api.user.GetBotInfoApi import love.forte.simbot.qguild.event.Shard +import love.forte.simbot.qguild.event.Signal import love.forte.simbot.qguild.model.User import love.forte.simbot.suspendrunner.ST import love.forte.simbot.suspendrunner.STP @@ -165,6 +166,38 @@ public interface Bot : CoroutineScope { @ST public suspend fun start(gateway: GatewayInfo) + /** + * 主动推送一个事件原文。 + * 可用于在 webhook 模式下推送事件。 + * + * @param payload 接收到的事件推送的JSON格式正文字符串。 + * @param options 额外提供的属性或配置。默认为 `null`。 + * + * @throws IllegalArgumentException 参考: + * - [EmitEventOptions.ignoreUnknownOpcode] + * - [EmitEventOptions.ignoreMissingOpcode] + * + * @since 4.1.0 + */ + @ST + public suspend fun emitEvent( + payload: String, + options: EmitEventOptions? = null, + ): EmitResult + + /** + * 主动推送一个事件原文。 + * 可用于在 webhook 模式下推送事件。 + * + * @param payload 接收到的事件推送的JSON格式正文字符串。 + * + * @since 4.1.0 + */ + @ST + public suspend fun emitEvent(payload: String): EmitResult { + return emitEvent(payload, null) + } + /** * 终止当前BOT。 */ @@ -195,7 +228,8 @@ public interface Bot : CoroutineScope { * 如果当前处于重连、重启的状态,得到的 [client] 中 [client.isActive][Client.isActive] 可能为 `false`, * [client] 本身也可能不存在。 * - * @return 当前bot持有的连接。如果当前正处于连接中、重连中或尚未启动,则可能得到null + * @return 当前bot持有的连接。如果当前正处于连接中、重连中、尚未启动或未启用ws连接, + * 则可能得到null */ public val client: Client? @@ -244,3 +278,92 @@ public enum class SubscribeSequence { */ NORMAL; } + +/** + * 使用 [Bot.emitEvent] 推送一个外部事件,并且在 [block] 中配置 [EmitEventOptions]。 + * @see Bot.emitEvent + * @since 4.1.0 + */ +public suspend inline fun Bot.emitEvent( + payload: String, + block: EmitEventOptions.() -> Unit +): EmitResult { + return emitEvent(payload, EmitEventOptions().apply(block)) +} + +/** + * 使用 [Bot.emitEvent] 推送事件后的结果, + * 会根据配置的不同和推送事件的 `opcode` 的不同得到不同的结果. + * + * @since 4.1.0 + */ +public sealed class EmitResult { + /** + * 不属于任何可处理的 `opcode`, 但在 [EmitEventOptions] + * 中配置了跳过而得到的无效返回值。 + */ + public data object Nothing : EmitResult() + + /** + * `opcode` 为 `0`, + * 普通的执行了事件调度、结束并返回。 + */ + public data object Dispatched : EmitResult() + + /** + * `opcode` 为 `13`, + * 对内容进行校验后得到了 [verified] 签名结果。 + */ + public data class Verified(val verified: Signal.CallbackVerify.Verified) : EmitResult() +} + +/** + * 在使用 [Bot.emitEvent] 时可选的一些额外属性或选项信息。 + * @since 4.1.0 + */ +public class EmitEventOptions { + /** + * 如果为 `true`, + * 则 payload 中解析出 [0, 13] 以外的 `op` 值不会抛出异常。 + * 默认为 `false` + */ + public var ignoreUnknownOpcode: Boolean = false + + /** + * 如果为 `true`, + * 则 payload 中不存在 `op` 值时不会抛出异常。 + * 默认为 `false` + */ + public var ignoreMissingOpcode: Boolean = false + + /** + * 如果需要对此回调事件进行 ed25519 签名校验, + * 则配置它所需的请求头透传参数。 + * + * 如果你想要以自己的逻辑提前校验则可设为 `null`, + * 如果为 `null` 则不会进行校验。 + * + * 默认为 `null`。 + * + * 更多参考 [官方文档](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/sign.html) + */ + public var ed25519SignatureVerification: Ed25519SignatureVerification? = null +} + +/** + * 进行 Ed25519 签名校验所需的参数。 + * + * 更多参考 [官方文档](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/sign.html) + * + * @since 4.1.0 + */ +public data class Ed25519SignatureVerification( + /** + * 来自请求头中的 `X-Signature-Ed25519`. + */ + val signatureEd25519: String, + /** + * 来自请求头中的 `X-Signature-Timestamp`. + */ + val signatureTimestamp: String, +) diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/BotConfiguration.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/BotConfiguration.kt index c154105f..884f2f84 100644 --- a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/BotConfiguration.kt +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/BotConfiguration.kt @@ -139,6 +139,14 @@ public interface BotConfiguration { */ public val wsClientEngineFactory: HttpClientEngineFactory<*>? + /** + * 是否禁用 ws 连接。如果你打算使用 webhook,则设置为 `true`, + * 届时在启动 bot 时不会再连接 ws 服务。 + * + * @since 4.1.0 + */ + public val disableWs: Boolean + /** * 用于API请求结果反序列化的 [Json]. * @@ -152,28 +160,6 @@ public interface BotConfiguration { * */ public val apiDecoder: Json -// -// /** -// * BOT内事件冲区的容量。 -// * -// * 缓冲区中堆积的事件如果已满则后续推送的事件会挂起等待缓冲区内元素的消费。 -// * -// * @see DEFAULT_EVENT_BUFFER_CAPACITY -// * -// */ -// public val eventBufferCapacity: Int -// -// -// public companion object { -// -// /** -// * 事件缓冲区的默认容量: `64` -// * -// * _此默认值没什么特殊含义,一拍脑袋想的。_ -// * -// */ -// public const val DEFAULT_EVENT_BUFFER_CAPACITY: Int = 64 -// } } diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration.kt index bf0069a1..d5c3805f 100644 --- a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration.kt +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/ConfigurableBotConfiguration.kt @@ -171,6 +171,8 @@ public class ConfigurableBotConfiguration : BotConfiguration { */ override var wsClientEngineFactory: HttpClientEngineFactory<*>? = null + override var disableWs: Boolean = false + /** * 用于API请求结果反序列化的 [Json]. * @@ -194,6 +196,7 @@ public class ConfigurableBotConfiguration : BotConfiguration { apiHttpSocketTimeoutMillis = apiHttpSocketTimeoutMillis, wsClientEngine = wsClientEngine, wsClientEngineFactory = wsClientEngineFactory, + disableWs = disableWs, apiDecoder = apiDecoder, ) @@ -214,5 +217,6 @@ private class BotConfigurationImpl( override val apiHttpSocketTimeoutMillis: Long?, override val wsClientEngine: HttpClientEngine?, override val wsClientEngineFactory: HttpClientEngineFactory<*>?, + override val disableWs: Boolean, override val apiDecoder: Json, ) : BotConfiguration diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotImpl.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotImpl.kt index f172f0a5..c278dcb3 100644 --- a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotImpl.kt +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotImpl.kt @@ -17,17 +17,21 @@ package love.forte.simbot.qguild.stdlib.internal +import com.ionspin.kotlin.crypto.signature.crypto_sign_BYTES import io.ktor.client.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.websocket.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.core.* import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import love.forte.simbot.common.atomic.AtomicLong import love.forte.simbot.common.atomic.atomic import love.forte.simbot.common.collection.ConcurrentQueue @@ -38,6 +42,7 @@ import love.forte.simbot.common.weak.WeakRef import love.forte.simbot.common.weak.weakRef import love.forte.simbot.logger.Logger import love.forte.simbot.logger.LoggerFactory +import love.forte.simbot.logger.isDebugEnabled import love.forte.simbot.qguild.QQGuildResultSerializationException import love.forte.simbot.qguild.api.GatewayApis import love.forte.simbot.qguild.api.GatewayInfo @@ -45,9 +50,7 @@ import love.forte.simbot.qguild.api.app.AppAccessToken import love.forte.simbot.qguild.api.app.GetAppAccessTokenApi import love.forte.simbot.qguild.api.requestData import love.forte.simbot.qguild.api.user.GetBotInfoApi -import love.forte.simbot.qguild.event.Ready -import love.forte.simbot.qguild.event.Shard -import love.forte.simbot.qguild.event.Signal +import love.forte.simbot.qguild.event.* import love.forte.simbot.qguild.model.User import love.forte.simbot.qguild.stdlib.* import love.forte.simbot.qguild.stdlib.DisposableHandle @@ -92,21 +95,35 @@ internal class BotImpl( private fun checkTicketSecret() { if (ticket.secret.isEmpty()) { - logger.error("The `ticket.secret` is empty. " + - "Since v4.0.0-beta6, the authentication logic within component " + - "has been migrated to new logic that requires the use of `secret`. " + - "If you do not configure the `ticket.secret`, " + - "it will most likely fail to start and throw an exception. " + - "See also: https://github.com/simple-robot/simbot-component-qq-guild/pull/163") + logger.error( + "The `ticket.secret` is empty. " + + "Since v4.0.0-beta6, the authentication logic within component " + + "has been migrated to new logic that requires the use of `secret`. " + + "If you do not configure the `ticket.secret`, " + + "it will most likely fail to start and throw an exception. " + + "See also: https://github.com/simple-robot/simbot-component-qq-guild/pull/163" + ) } } - internal val wsDecoder = Signal.Dispatch.dispatchJson { + private val ed25519KeyPair: Ed25519Keypair by lazy { + genEd25519Keypair(ticket.secret.paddingEd25519Seed().toByteArray()) + } + + private val ed25519PrivateKey + get() = ed25519KeyPair.privateKey + + private val ed25519PublicKey + get() = ed25519KeyPair.publicKey + + internal val eventDecoder = Signal.Dispatch.dispatchJson { isLenient = true ignoreUnknownKeys = true } - internal val wsClient: HttpClient = configuration.let { + private val wsClient: HttpClient? = configuration.let { + if (it.disableWs) return@let null + val wsClientEngine = it.wsClientEngine val wsClientEngineFactory = it.wsClientEngineFactory @@ -125,7 +142,6 @@ internal class BotImpl( } } - private fun HttpClientConfig<*>.configWsHttpClient() { WebSockets { } @@ -296,37 +312,45 @@ internal class BotImpl( flushAccessTokenJob = initFlushAccessTokenJob() } - val gateway = gatewayFactory() + if (wsClient != null) { + val gateway = gatewayFactory() - logger.debug("Request gateway {} by shard {}", gateway, shard) + logger.debug("Request gateway {} by shard {}", gateway, shard) - val state = Connect(this, shard, gateway) + this.stageLoopJob = launchWsLoop(wsClient, gateway) + } else { + logger.debug("WsClient is null, will not connect to ws server.") + } + } + } - logger.debug("Create state loop {}", state) + private suspend fun launchWsLoop(wsClient: HttpClient, gateway: GatewayInfo): Job { + val state = Connect(this, shard, gateway, wsClient) - var st: State? = state - do { - logger.debug("Current state: {}", st) - st = st?.invoke() - } while (st != null && st !is ReceiveEvent) + logger.debug("Create state loop {}", state) - if (st == null) { - // 当前状态为空且尚未进入事件接收状态 - throw IllegalStateException("The current state is null and not yet in the event receiving state") - } + var st: State? = state + do { + logger.debug("Current state: {}", st) + st = st?.invoke() + } while (st != null && st !is ReceiveEvent) - val stageLoopJob: Job = launch { - st.loop() - } + if (st == null) { + // 当前状态为空且尚未进入事件接收状态 + throw IllegalStateException("The current state is null and not yet in the event receiving state") + } - stageLoopJob.invokeOnCompletion { reason -> - reason?.also { - logger.debug("StageLoopJob is on completion: {}", it.message, it) - } - } + val stageLoopJob: Job = launch { + st.loop() + } - this.stageLoopJob = stageLoopJob + stageLoopJob.invokeOnCompletion { reason -> + reason?.also { + logger.debug("StageLoopJob is on completion: {}", it.message, it) + } } + + return stageLoopJob } private suspend fun initFlushAccessTokenJob(): Job { @@ -387,6 +411,112 @@ internal class BotImpl( } } + @OptIn(ExperimentalStdlibApi::class) + override suspend fun emitEvent( + payload: String, + options: EmitEventOptions?, + ): EmitResult { + // CODE 名称 客户端行为 描述 + // 0 Dispatch Receive 服务端进行消息推送 + // 13 回调地址验证 Receive 开放平台对机器人服务端进行验证 + logger.debug("Emit raw event with payload: {}", payload) + + fun verifyIfNecessary() { + val (signature, signatureTimestamp) = options?.ed25519SignatureVerification ?: return + logger.debug("`ed25519SignatureVerification` exists, verity the payload") + + val signatureBytes = signature.hexToByteArray() + + check(crypto_sign_BYTES == signatureBytes.size) { + "Invalid signature hex size, " + + "expect ${crypto_sign_BYTES}, " + + "actual ${signatureBytes.size}" + } + + val and: UByte = signatureBytes[63].toUByte().and(224u) + + check(and == 0u.toUByte()) { + "Invalid signature hex, " + + "expect signatureBytes[63] && 224 == 0, " + + "actual $and (0x${and.toHexString()})" + } + + val msg = "$signatureTimestamp$payload" + + check(ed25519PublicKey.verify(msg.toByteArray(), signatureBytes)) { + "Ed25519 verify failed" + } + } + + fun signatureIfNecessary(verify: Signal.CallbackVerify): EmitResult { + val (plainToken, eventTs) = verify.data + val msg = "$eventTs$plainToken" + + val signature = ed25519PrivateKey.sign(msg.toByteArray()) + + val verified = Signal.CallbackVerify.Verified( + plainToken, + signature.signatureBytes().toHexString() + ) + + return EmitResult.Verified(verified) + } + + // Verify payload + verifyIfNecessary() + + val json = eventDecoder.parseToJsonElement(payload) + + when (val opcode = json.getOpcode()) { + null -> { + if (options?.ignoreMissingOpcode == true) return EmitResult.Nothing + + throw IllegalArgumentException("Required attribute `$.op` is missing") + } + + Opcodes.Dispatch -> { + val serializer = resolveDispatchSerializer( + json = json.jsonObject, + allowNameMissing = true + ) + + val dispatch = if (serializer != null) { + eventDecoder.decodeFromJsonElement(serializer, json) + } else { + // Unknown + val disSeq = -1L + val id = json.tryGetId() + + Signal.Dispatch.Unknown(id, disSeq, json, payload).also { + val t = + kotlin.runCatching { + json.jsonObject[Signal.Dispatch.DISPATCH_CLASS_DISCRIMINATOR] + ?.jsonPrimitive + ?.content + }.getOrNull() + + logger.warn("Unknown event type {}, decode it as Unknown event: {}", t, it) + } + } + + emitEvent(dispatch, payload) + return EmitResult.Dispatched + } + + Opcodes.CallbackVerify -> { + val verify = eventDecoder.decodeFromJsonElement(Signal.CallbackVerify.serializer(), json) + logger.debug("On Signal.CallbackVerify: {}", verify) + return signatureIfNecessary(verify) + } + + else -> { + if (options?.ignoreUnknownOpcode == true) return EmitResult.Nothing + + throw IllegalArgumentException("Unknown opcode: $opcode, emitEvent can only support opcode in [0, 13]") + } + } + } + override fun cancel(reason: Throwable?) { if (!job.isActive) return @@ -460,3 +590,62 @@ internal data class SessionInfo( val readyData: Ready.Data, ) +@OptIn(ExperimentalSimbotCollectionApi::class) +internal suspend fun BotImpl.emitEvent(dispatch: Signal.Dispatch, raw: String) { + logger.debug("Emit event {} from raw {}", dispatch, raw) + // 先顺序地使用 preProcessor 处理 + preProcessorQueue.forEach { processor -> + runCatching { + processor.doInvoke(dispatch, raw) + }.onFailure { e -> + if (logger.isDebugEnabled) { + logger.debug( + "Event pre-precess failure. raw: {}, event: {}", raw, dispatch + ) + } + logger.error("Event pre-precess failure.", e) + } + } + + // 然后异步地正常处理 + // TODO 同步异步 configurable? + // bot launch or session launch? + launch { + processorQueue.forEach { processor -> + runCatching { + processor.doInvoke(dispatch, raw) + }.onFailure { e -> + if (logger.isDebugEnabled) { + logger.debug( + "Event precess failure. raw: {}, event: {}", raw, dispatch + ) + } + logger.error("Event precess failure.", e) + } + } + } +} + + +/** + * Repeat to `length` == 32 + */ +internal fun String.paddingEd25519Seed(): String { + return when { + length == 32 || isEmpty() -> this + length > 32 -> substring(32) + else -> { + buildString(32) { + append(this@paddingEd25519Seed) + while (length < 32) { + append( + this@paddingEd25519Seed, + 0, + kotlin.math.min(32 - length, this@paddingEd25519Seed.length) + ) + } + } + + } + } +} diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotStates.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotStates.kt index 2be79583..1ebc30ff 100644 --- a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotStates.kt +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/BotStates.kt @@ -24,8 +24,9 @@ import io.ktor.websocket.* import kotlinx.coroutines.* import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.* -import love.forte.simbot.common.collection.ExperimentalSimbotCollectionApi +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import love.forte.simbot.logger.Logger import love.forte.simbot.logger.isDebugEnabled import love.forte.simbot.logger.isTraceEnabled @@ -33,7 +34,6 @@ import love.forte.simbot.qguild.api.GatewayApis import love.forte.simbot.qguild.api.GatewayInfo import love.forte.simbot.qguild.err import love.forte.simbot.qguild.event.* -import love.forte.simbot.qguild.stdlib.doInvoke import love.forte.simbot.qguild.stdlib.internal.BotImpl.ClientImpl import love.forte.simbot.qguild.stdlib.requestDataBy import kotlin.math.max @@ -80,6 +80,7 @@ internal class Connect( override val bot: BotImpl, private val shard: Shard, private val gateway: GatewayInfo, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State { val intents = bot.configuration.intents @@ -108,11 +109,11 @@ internal class Connect( ) bot.logger.debug("Connect to ws with gateway {}", gateway) - val session = bot.wsClient.ws { gateway } + val session = wsClient.ws { gateway } // next: receive Hello return WaitingHello(bot, session) { hello -> - WaitingReadyEvent(bot, identify, hello.data, session) + WaitingReadyEvent(bot, identify, hello.data, session, wsClient) } } } @@ -128,9 +129,9 @@ internal class WaitingHello( val frame = session.incoming.receive() as? Frame.Text ?: continue val text = frame.readText() logger.debug("Waiting hello : received frame {}", text) - val json = bot.wsDecoder.parseToJsonElement(text) + val json = bot.eventDecoder.parseToJsonElement(text) if (json.jsonObject["op"]?.jsonPrimitive?.int == Opcodes.Hello) { - hello = bot.wsDecoder.decodeFromJsonElement(Signal.Hello.serializer(), json) + hello = bot.eventDecoder.decodeFromJsonElement(Signal.Hello.serializer(), json) break } } @@ -157,26 +158,16 @@ internal class WaitingReadyEvent( override val bot: BotImpl, private val identify: Signal.Identify, private val hello: Signal.Hello.Data, - private val session: DefaultClientWebSocketSession + private val session: DefaultClientWebSocketSession, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State { // 发送 identify - val identifyJson = bot.wsDecoder.encodeToString(Signal.Identify.serializer(), identify) + val identifyJson = bot.eventDecoder.encodeToString(Signal.Identify.serializer(), identify) logger.debug("Send identify {}, JSON: {}", identify, identifyJson) session.send(identifyJson) logger.debug("Send identify Successfully.") -// HeartbeatJob( -// bot, hello, -// Ready.Data( -// "", -// "", -// User("", ""), -// Shard.FULL -// ), session -// ).invoke() - - // 等待ready var ready: Ready? = null logger.debug("Waiting for Signal ready...") @@ -191,9 +182,13 @@ internal class WaitingReadyEvent( val frame = frameResult.getOrThrow() as? Frame.Text ?: continue val text = frame.readText() logger.debug("Waiting ready event : received frame {}", text) - val json = bot.wsDecoder.parseToJsonElement(text) - if (json.jsonObject["op"]?.jsonPrimitive?.int == Opcodes.Dispatch) { - val dispatch = bot.wsDecoder.decodeFromJsonElement(Signal.Dispatch.serializer(), json) + val json = bot.eventDecoder.parseToJsonElement(text) + if (json.getOpcode() == Opcodes.Dispatch) { + val dispatchSerializer = resolveDispatchSerializer(json.jsonObject) + ?: continue + + val dispatch = bot.eventDecoder.decodeFromJsonElement(dispatchSerializer, json) + if (dispatch is Ready) { ready = dispatch break @@ -210,7 +205,7 @@ internal class WaitingReadyEvent( logger.debug("Received Ready event: {}", r) // next: session - return HeartbeatJob(bot, hello, r.data, session) + return HeartbeatJob(bot, hello, r.data, session, wsClient) } } @@ -219,7 +214,8 @@ internal class HeartbeatJob( override val bot: BotImpl, private val hello: Signal.Hello.Data, private val readyData: Ready.Data, - private val session: DefaultClientWebSocketSession + private val session: DefaultClientWebSocketSession, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State { val seq = AtomicLongRef(-1L) @@ -230,7 +226,7 @@ internal class HeartbeatJob( val sessionInfo = SessionInfo(session, seq, heartbeatJob, logger, readyData) // next: process event - return CreateClient(bot, sessionInfo, session) + return CreateClient(bot, sessionInfo, session, wsClient) } @@ -252,7 +248,7 @@ internal class HeartbeatJob( } delay(timeMillis) val hb = Signal.Heartbeat(seq.value.takeIf { it >= 0 }) - session.send(bot.wsDecoder.encodeToString(serializer, hb)) + session.send(bot.eventDecoder.encodeToString(serializer, hb)) } } } @@ -265,7 +261,8 @@ internal class HeartbeatJob( internal class CreateClient( override val bot: BotImpl, private val botClientSession: SessionInfo, - private val session: DefaultClientWebSocketSession + private val session: DefaultClientWebSocketSession, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State { val client = bot.ClientImpl( @@ -280,7 +277,7 @@ internal class CreateClient( bot.updateClient(client) // next: receive events - return ReceiveEvent(bot, client) + return ReceiveEvent(bot, client, wsClient) } } @@ -290,6 +287,7 @@ internal class CreateClient( internal class ReceiveEvent( override val bot: BotImpl, private val client: ClientImpl, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State? { val session = client.wsSession @@ -297,7 +295,7 @@ internal class ReceiveEvent( if (!session.isActive) { val reason = session.closeReason.await() logger.error("Session is closed. reason: {}. Try to resume", reason) - return Resume(bot, client) + return Resume(bot, client, wsClient) } suspend fun onCatchErr(e: Throwable?): State? { @@ -305,13 +303,13 @@ internal class ReceiveEvent( if (reason == null) { logger.debug("Session closed and reason is null, try to resume", e) // try resume - return Resume(bot, client) + return Resume(bot, client, wsClient) } suspend fun doIdentify(): State { val gatewayInfo: GatewayInfo = GatewayApis.Normal.requestDataBy(bot) logger.debug("Reconnect gateway {} by shard {}", gatewayInfo, bot.shard) - return Connect(bot, bot.shard, gatewayInfo) + return Connect(bot, bot.shard, gatewayInfo, wsClient) } val reasonCode = reason.code @@ -319,7 +317,7 @@ internal class ReceiveEvent( canBeResumed(reasonCode) -> { logger.debug("Session closed({}), try to resume", reason, e) // try resume - return Resume(bot, client) + return Resume(bot, client, wsClient) } canBeIdentified(reasonCode) -> { @@ -376,44 +374,39 @@ internal class ReceiveEvent( return this } logger.debug("Received text frame raw: {}", raw) - val json = bot.wsDecoder.parseToJsonElement(raw) + val json = bot.eventDecoder.parseToJsonElement(raw) when (val opcode = json.getOpcode()) { Opcodes.Dispatch -> { // event - val dispatch = try { - bot.wsDecoder.decodeFromJsonElement(Signal.Dispatch.serializer(), json) - } catch (serEx: SerializationException) { -// if (tryCheckIsPolymorphicException(serEx)) { + val serializer = resolveDispatchSerializer( + json = json.jsonObject, + allowNameMissing = true + ) + + val dispatch = if (serializer != null) { + bot.eventDecoder.decodeFromJsonElement(serializer, json) + } else { // 未知的事件类型 - val disSeq = runCatching { - json.jsonObject["s"]?.jsonPrimitive?.longOrNull ?: seq.value - }.getOrElse { seq.value } - // dispatch.id - val id = kotlin.runCatching { - json.jsonObject["id"]?.jsonPrimitive?.contentOrNull - }.getOrNull() + val disSeq = json.tryGetSeq() ?: seq.value + val id = json.tryGetId() Signal.Dispatch.Unknown(id, disSeq, json, raw).also { val t = - kotlin.runCatching { json.jsonObject[Signal.Dispatch.DISPATCH_CLASS_DISCRIMINATOR]?.jsonPrimitive?.content } - .getOrNull() - if (tryCheckIsPolymorphicException(serEx)) { - logger.warn("Unknown event type {}, decode it as Unknown event: {}", t, it) - } else { - logger.warn("Unknown event type {}, decode it as Unknown event: {}", t, it, serEx) - } + kotlin.runCatching { + json.jsonObject[Signal.Dispatch.DISPATCH_CLASS_DISCRIMINATOR] + ?.jsonPrimitive + ?.content + }.getOrNull() + + logger.warn("Unknown event type {}, decode it as Unknown event: {}", t, it) } -// } else { -// // throw out -// throw serEx -// } } + logger.debug("Received dispatch: {}", dispatch) val dispatchSeq = dispatch.seq // 推送事件 - emitEvent(bot, dispatch, raw) -// eventSharedFlow.emit(EventData(raw, dispatch)) + bot.emitEvent(dispatch, raw) // seq留下最大值 val currentSeq = seq.updateAndGet { pref -> max(pref, dispatchSeq) } @@ -423,7 +416,7 @@ internal class ReceiveEvent( Opcodes.Reconnect -> { // 重新连接 logger.debug("Received reconnect signal. Do Resume.") - return Resume(bot, client) + return Resume(bot, client, wsClient) } else -> { @@ -441,7 +434,8 @@ internal class ReceiveEvent( } } -private fun tryCheckIsPolymorphicException(exception: SerializationException): Boolean { +@Deprecated("Deprecated") +internal fun tryCheckIsPolymorphicException(exception: SerializationException): Boolean { // 似乎是某个版本的错误提示,但是至少在 JSON v1.6.3 已经不适用了 if (exception.message?.startsWith("Polymorphic serializer was not found for") == true) { return true @@ -455,42 +449,6 @@ private fun tryCheckIsPolymorphicException(exception: SerializationException): B return false } -@OptIn(ExperimentalSimbotCollectionApi::class) -private suspend fun emitEvent(bot: BotImpl, dispatch: Signal.Dispatch, raw: String) { - val logger = bot.logger - // 先顺序地使用 preProcessor 处理 - bot.preProcessorQueue.forEach { processor -> - runCatching { - processor.doInvoke(dispatch, raw) - }.onFailure { e -> - if (logger.isDebugEnabled) { - logger.debug( - "Event pre-precess failure. raw: {}, event: {}", raw, dispatch - ) - } - logger.error("Event pre-precess failure.", e) - } - } - - // 然后异步地正常处理 - // TODO 同步异步 configurable? - // bot launch or session launch? - bot.launch { - bot.processorQueue.forEach { processor -> - runCatching { - processor.doInvoke(dispatch, raw) - }.onFailure { e -> - if (logger.isDebugEnabled) { - logger.debug( - "Event precess failure. raw: {}, event: {}", raw, dispatch - ) - } - logger.error("Event precess failure.", e) - } - } - } -} - /** * [4. 恢复连接](https://bot.q.qq.com/wiki/develop/api/gateway/reference.html#_4-%E6%81%A2%E5%A4%8D%E8%BF%9E%E6%8E%A5) @@ -502,23 +460,24 @@ private suspend fun emitEvent(bot: BotImpl, dispatch: Signal.Dispatch, raw: Stri internal class Resume( override val bot: BotImpl, private val client: ClientImpl, + private val wsClient: HttpClient, ) : State() { override suspend fun invoke(): State { val newSession = bot.startLock.withLock { // 关闭当前连接 client.cancelAndJoin() val gateway = GatewayApis.Normal.requestDataBy(bot) - bot.wsClient.ws { gateway }.apply { + wsClient.ws { gateway }.apply { // 发送 Opcode6 val resumeSignal = Signal.Resume(Signal.Resume.Data(bot.qqBotToken, client.readyData.sessionId, client.seq)) - send(bot.wsDecoder.encodeToString(Signal.Resume.serializer(), resumeSignal)) + send(bot.eventDecoder.encodeToString(Signal.Resume.serializer(), resumeSignal)) } } return WaitingHello(bot, newSession) { hello -> // 跳过 wait ready, 直接HeartbeatJob - HeartbeatJob(bot, hello.data, client.readyData, session) + HeartbeatJob(bot, hello.data, client.readyData, session, wsClient) } } } diff --git a/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/Ed25519s.kt b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/Ed25519s.kt new file mode 100644 index 00000000..45f31cf2 --- /dev/null +++ b/simbot-component-qq-guild-stdlib/src/commonMain/kotlin/love/forte/simbot/qguild/stdlib/internal/Ed25519s.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-qq-guild. + * + * simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild. + * If not, see . + */ + +package love.forte.simbot.qguild.stdlib.internal + +import com.ionspin.kotlin.crypto.LibsodiumInitializer +import com.ionspin.kotlin.crypto.signature.InvalidSignatureException +import com.ionspin.kotlin.crypto.signature.Signature +import com.ionspin.kotlin.crypto.signature.SignatureKeyPair +import com.ionspin.kotlin.crypto.signature.crypto_sign_BYTES +import love.forte.simbot.common.atomic.atomic +import love.forte.simbot.logger.LoggerFactory +import kotlin.jvm.JvmInline + +@JvmInline +@OptIn(ExperimentalUnsignedTypes::class) +internal value class Ed25519Keypair(private val keyPair: SignatureKeyPair) { + val publicKey: Ed25519PublicKey + get() = Ed25519PublicKey(keyPair.publicKey) + + val privateKey: Ed25519PrivateKey + get() = Ed25519PrivateKey(keyPair.secretKey) +} + +@JvmInline +@OptIn(ExperimentalUnsignedTypes::class) +internal value class Ed25519PrivateKey(private val key: UByteArray) { + fun sign(msg: ByteArray): SignResult = + SignResult(Signature.sign(msg.asUByteArray(), key)) + + @JvmInline + internal value class SignResult(private val signature: UByteArray) { + fun signatureUBytes(): UByteArray = + signature.copyOf(crypto_sign_BYTES) + + fun signatureBytes(): ByteArray = + signatureUBytes().asByteArray() + + fun plainUBytes(): UByteArray = + signature.copyOfRange(crypto_sign_BYTES, signature.size) + + fun plainBytes(): ByteArray = + plainUBytes().asByteArray() + } +} + +@JvmInline +@OptIn(ExperimentalUnsignedTypes::class) +internal value class Ed25519PublicKey(private val key: UByteArray) { + fun verify(signature: ByteArray, message: ByteArray): Boolean { + return try { + Signature.verifyDetached( + signature.asUByteArray(), + message.asUByteArray(), + key + ) + true + } catch (ise: InvalidSignatureException) { + false + } + + } +} + +@OptIn(ExperimentalUnsignedTypes::class) +internal fun genEd25519Keypair(seed: ByteArray): Ed25519Keypair { + initialed + + val pk = Signature.seedKeypair(seed.asUByteArray()) + return Ed25519Keypair(pk) +} + +private val Ed25519Logger = LoggerFactory.getLogger("love.forte.simbot.qguild.stdlib.internal.Ed25519") + +private val initialed: Unit by lazy( + mode = LazyThreadSafetyMode.SYNCHRONIZED +) { + if (!LibsodiumInitializer.isInitialized()) { + Ed25519Logger.info("LibsodiumInitializer is not initialed yet, initializing...") + val done = atomic(false) + LibsodiumInitializer.initializeWithCallback { + Ed25519Logger.info("LibsodiumInitializer initialized in callback") + done.value = true + } + + @Suppress("ControlFlowWithEmptyBody") + while (!done.value) { + } + + Ed25519Logger.info("LibsodiumInitializer initialized") + } + + Unit +} diff --git a/simbot-component-qq-guild-stdlib/src/commonTest/kotlin/Ed25519Tests.kt b/simbot-component-qq-guild-stdlib/src/commonTest/kotlin/Ed25519Tests.kt new file mode 100644 index 00000000..19020258 --- /dev/null +++ b/simbot-component-qq-guild-stdlib/src/commonTest/kotlin/Ed25519Tests.kt @@ -0,0 +1,182 @@ +import com.ionspin.kotlin.crypto.LibsodiumInitializer +import com.ionspin.kotlin.crypto.signature.Signature +import com.ionspin.kotlin.crypto.signature.crypto_sign_BYTES +import io.ktor.client.engine.mock.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.test.runTest +import love.forte.simbot.qguild.stdlib.BotFactory +import love.forte.simbot.qguild.stdlib.EmitResult +import love.forte.simbot.qguild.stdlib.internal.paddingEd25519Seed +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertIs + +/** + * + * @author ForteScarlet + */ +class Ed25519TestsCommon { + + +// @OptIn(ExperimentalStdlibApi::class) +// @Test +// fun ed25519KeyGenTest() { +// // val secret = "naOC0ocQE3shWLAfffVLB1rhYPG7" +// val seed = "naOC0ocQE3shWLAfffVLB1rhYPG7naOC" +// +// +// +// val pk = Ed25519.keyFromSeed(seed.toByteArray()) +// +// assertContentEquals( +// ASSERT_PUBLIC_HEX.hexToByteArray(), +// pk.publicKey().toByteArray() +// ) +// +// assertContentEquals( +// ASSERT_PRIVATE_HEX.hexToByteArray(), +// pk.toByteArray() +// ) +// } + +// @BeforeTest +// fun initLibsodiumInitializer() = runTest { +// if (!LibsodiumInitializer.isInitialized()) { +// LibsodiumInitializer.initialize() +// } +// } + + /** + * https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/sign.html + * + * 验证签名过程: + * 根据开发者平台的 Bot Secret 值进行repeat操作得到签名32字节的 seed , + * 根据 seed 调用 Ed25519 算法生成32字节公钥 + */ + @OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) + @Test + fun libsodiumEd25519KeyGenTest() = runTest { + LibsodiumInitializer.initialize() + // val secret = "naOC0ocQE3shWLAfffVLB1rhYPG7" + val seed = "naOC0ocQE3shWLAfffVLB1rhYPG7naOC" + + val pk = Signature.seedKeypair(seed.toByteArray().asUByteArray()) + + assertContentEquals( + ASSERT_PUBLIC_HEX.hexToByteArray(), + pk.publicKey.asByteArray() + ) + + assertContentEquals( + ASSERT_PRIVATE_HEX.hexToByteArray(), + pk.secretKey.asByteArray() + ) + } + + @Test + fun ed25519SeedPadding() { + assertEquals( + "DG5g3B4j9X2KOErGDG5g3B4j9X2KOErG", + "DG5g3B4j9X2KOErG".paddingEd25519Seed() + ) + assertEquals( + "DG5g3B4j9X2KOErGDG5g3B4j9X2KOErG", + "DG5g3B4j9X2KOErGDG5g3B4j9X2KOErGDG5g3B4j9X2KOErGDG5g3B4j9X2KOErG".paddingEd25519Seed() + ) + } + +// @OptIn(ExperimentalStdlibApi::class) +// @Test +// fun ed25519VerifyTest() { +// // val appId = "11111111" +// val secret = "DG5g3B4j9X2KOErG" +// val seed = secret.paddingEd25519Seed() +// val plainToken = "Arq0D5A61EgUu4OxUvOp" +// val ts = "1725442341" +// // val body = """{"d":{"plain_token":"Arq0D5A61EgUu4OxUvOp","event_ts":"1725442341"},"op":13}""" +// +// val pk = Ed25519.keyFromSeed(seed.toByteArray()) +// val msg = "$ts$plainToken" +// +// val signature = pk.sign(msg.toByteArray()) +// +// assertEquals( +// "87befc99c42c651b3aac0278e71ada338433ae26fcb24307bdc5ad38c1adc2d01bcfcadc0842edac85e85205028a1132afe09280305f13aa6909ffc2d652c706", +// signature.toHexString() +// ) +// } + + @OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) + @Test + fun libsodiumEd25519VerifyTest() { + // val appId = "11111111" + val secret = "DG5g3B4j9X2KOErG" + val seed = secret.paddingEd25519Seed() + val plainToken = "Arq0D5A61EgUu4OxUvOp" + val ts = "1725442341" + // val body = """{"d":{"plain_token":"Arq0D5A61EgUu4OxUvOp","event_ts":"1725442341"},"op":13}""" + + val pk = Signature.seedKeypair(seed.toByteArray().asUByteArray()) +// val pk = Ed25519.keyFromSeed(seed.toByteArray()) + + val msg = "$ts$plainToken" + + // 得到的是加密的密文 (size=crypto_sign_BYTES) + // 和原文 (size=msg.size) + val signature = Signature.sign( + msg.toByteArray().asUByteArray(), + pk.secretKey + ) + + val plain = signature.copyOfRange(crypto_sign_BYTES, signature.size) + + assertEquals( + msg.toByteArray().toHexString(), + plain.toHexString() + ) + +// val signature = pk.sign(msg.toByteArray()) + + assertEquals( + "87befc99c42c651b3aac0278e71ada338433ae26fcb24307bdc5ad38c1adc2d01bcfcadc0842edac85e85205028a1132afe09280305f13aa6909ffc2d652c706", + signature.copyOf(crypto_sign_BYTES).toHexString() + ) + } + + @Test + fun opcode13ProcessTest() = runTest { + val bot = BotFactory.create("11111111", "DG5g3B4j9X2KOErG", "") { + disableWs = true + apiClientEngine = MockEngine { + respondOk() + } + } + + + val result = bot.emitEvent( + """{"d":{"plain_token":"Arq0D5A61EgUu4OxUvOp","event_ts":"1725442341"},"op":13}""", + ) + + assertIs(result) + assertEquals( + "87befc99c42c651b3aac0278e71ada338433ae26fcb24307bdc5ad38c1adc2d01bcfc" + + "adc0842edac85e85205028a1132afe09280305f13aa6909ffc2d652c706", + result.verified.signature, + "Verified signature not equal" + ) + assertEquals( + "Arq0D5A61EgUu4OxUvOp", + result.verified.plainToken, + "Verified plainToken not equal" + ) + } + + companion object { + private const val ASSERT_PUBLIC_HEX = "d7c362fe78aef81ff23287b493628b5db02a3c4fe30b215e4d19609b5d76673a" + private const val ASSERT_PRIVATE_HEX = + "6e614f43306f635145337368574c41666666564c42317268595047376e614f43" + + "d7c362fe78aef81ff23287b493628b5db02a3c4fe30b215e4d19609b5d76673a" + } +} +