diff --git a/internal-processors/message-element-processor/build.gradle.kts b/internal-processors/message-element-processor/build.gradle.kts new file mode 100644 index 00000000..be27cc4d --- /dev/null +++ b/internal-processors/message-element-processor/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook 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-kook 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-kook, + * If not, see . + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(11) + compilerOptions { + javaParameters = true + jvmTarget.set(JvmTarget.JVM_11) + } +} + +configJavaCompileWithModule() + +dependencies { + api(libs.ksp) + api(libs.kotlinPoet.ksp) + testImplementation(kotlin("test-junit5")) +} + +tasks.getByName("test") { + useJUnitPlatform() +} + diff --git a/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessor.kt b/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessor.kt new file mode 100644 index 00000000..6007900b --- /dev/null +++ b/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessor.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook 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-kook 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-kook, + * If not, see . + */ + +package kook.internal.processors.msgelement + +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.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 BASE_ELEMENT_CLASS_PACKAGE = "love.forte.simbot.component.kook.message" +private const val BASE_ELEMENT_CLASS_SIMPLE_NAME = "KookMessageElement" + +private const val BASE_ELEMENT_CLASS_NAME = + "$BASE_ELEMENT_CLASS_PACKAGE.$BASE_ELEMENT_CLASS_SIMPLE_NAME" + +private val BaseElementClassName = + ClassName(BASE_ELEMENT_CLASS_PACKAGE, BASE_ELEMENT_CLASS_SIMPLE_NAME) + +private const val KTX_SERIALIZABLE_ANNOTATION_PACKAGE = "kotlinx.serialization" +private const val KTX_SERIALIZABLE_ANNOTATION_SIMPLE_NAME = "Serializable" +private const val KTX_SERIALIZABLE_ANNOTATION_NAME = + "$KTX_SERIALIZABLE_ANNOTATION_PACKAGE.$KTX_SERIALIZABLE_ANNOTATION_SIMPLE_NAME" + +private val PolymorphicModuleBuilderClassName = + ClassName("kotlinx.serialization.modules", "PolymorphicModuleBuilder") + +private const val OUTPUT_PACKAGE = "love.forte.simbot.component.kook.message" +private const val OUTPUT_FILE = "IncludeKookMessageElements.generated" +private const val OUTPUT_FUN_NAME = "includeKookMessageElements" + +/** + * + * @author ForteScarlet + */ +class MessageElementProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + private val codeGenerator = environment.codeGenerator + private val elementDeclarations = mutableListOf() + + override fun finish() { + val newFun = resolve(elementDeclarations) + + val fileSpec = FileSpec.Companion.builder( + OUTPUT_PACKAGE, OUTPUT_FILE + ) + .addFunction(newFun) + .addFileComment("\nAuto-Generated at ${Instant.now().atOffset(ZoneOffset.ofHours(8))}\n") + .build() + + fileSpec.writeTo( + codeGenerator = codeGenerator, + aggregating = true, + originatingKSFiles = buildList { + for (impl in elementDeclarations) { + impl.containingFile?.also { add(it) } + } + } + ) + } + + override fun process(resolver: Resolver): List { + val baseClass = resolver.getClassDeclarationByName( + BASE_ELEMENT_CLASS_NAME + ) ?: error("Base class $BASE_ELEMENT_CLASS_NAME not found") + + val baseClassStarType = baseClass.asStarProjectedType() + + resolver + .getSymbolsWithAnnotation(KTX_SERIALIZABLE_ANNOTATION_NAME) + .filterIsInstance() + .filter { declaration -> + // 是 base class 的子类 + baseClassStarType.isAssignableFrom(declaration.asStarProjectedType()) + } + .filter { declaration -> + // 不是抽象的、不是sealed的可序列化类 + !declaration.isAbstract() && !declaration.modifiers.contains(Modifier.SEALED) + } + .toCollection(elementDeclarations) + + return emptyList() + } + + /** + * 生成函数: + * + * ```kotlin + * internal fun PolymorphicModuleBuilder.includeKookMessageElements() { + * subclass(KookAsset::class, KookAsset.serializer()) + * // ... + * } + * ``` + */ + private fun resolve(declarations: List): FunSpec { + val receiverType = PolymorphicModuleBuilderClassName.parameterizedBy(BaseElementClassName) +// val optAnnotation = + val optAnnotation = AnnotationSpec.builder(ClassName("kotlin", "OptIn")) + .addMember( + "%T::class, %T::class", + ClassName("love.forte.simbot.annotations", "ExperimentalSimbotAPI"), + ClassName("love.forte.simbot.kook", "ExperimentalKookApi") + ) + .build() + + val internalAPIAnnotation = ClassName("love.forte.simbot.kook", "InternalKookApi") + + return FunSpec.builder(OUTPUT_FUN_NAME).apply { + modifiers.add(KModifier.INTERNAL) + receiver(receiverType) + addAnnotation(optAnnotation) + addAnnotation(internalAPIAnnotation) + + for (declaration in declarations) { + val className = declaration.toClassName() + addCode("subclass(%T::class, %T.serializer())\n", className, className) + } + }.build() + } +} + diff --git a/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessorProvider.kt b/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessorProvider.kt new file mode 100644 index 00000000..972199da --- /dev/null +++ b/internal-processors/message-element-processor/src/main/kotlin/kook/internal/processors/msgelement/MessageElementProcessorProvider.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook 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-kook 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-kook, + * If not, see . + */ + +package kook.internal.processors.msgelement + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +/** + * + * @author ForteScarlet + */ +class MessageElementProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = + MessageElementProcessor(environment) +} + diff --git a/internal-processors/message-element-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/internal-processors/message-element-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000..6bb0a159 --- /dev/null +++ b/internal-processors/message-element-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +kook.internal.processors.msgelement.MessageElementProcessorProvider diff --git a/settings.gradle.kts b/settings.gradle.kts index edc33e40..a61f8bac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,24 +1,28 @@ /* - * Copyright (c) 2022-2023. ForteScarlet. + * Copyright (c) 2022-2024. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook 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-kook 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-kook 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. + * simbot-component-kook 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-kook, - * If not, see . + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . */ rootProject.name = "simbot-component-kook" // internals include(":internal-processors:api-reader") +include(":internal-processors:message-element-processor") include("simbot-component-kook-api") include("simbot-component-kook-stdlib") diff --git a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/Kook.kt b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/Kook.kt index d94bab92..e97af2f9 100644 --- a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/Kook.kt +++ b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/Kook.kt @@ -1,18 +1,21 @@ /* - * Copyright (c) 2023. ForteScarlet. + * Copyright (c) 2023-2024. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook 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-kook 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-kook 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. + * simbot-component-kook 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-kook, - * If not, see . + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . */ package love.forte.simbot.kook @@ -82,6 +85,7 @@ public object Kook { useArrayPolymorphism = false // see https://github.com/kaiheila/api-docs/issues/174 explicitNulls = false + coerceInputValues = true } } diff --git a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/messages/MessageDetails.kt b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/messages/MessageDetails.kt index d5d7a609..fbd75151 100644 --- a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/messages/MessageDetails.kt +++ b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/messages/MessageDetails.kt @@ -1,24 +1,30 @@ /* - * Copyright (c) 2021-2023. ForteScarlet. + * Copyright (c) 2021-2024. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook 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-kook 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-kook 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. + * simbot-component-kook 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-kook, - * If not, see . + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . */ package love.forte.simbot.kook.messages import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import love.forte.simbot.kook.Kook import love.forte.simbot.kook.api.ApiResultType import love.forte.simbot.kook.objects.* @@ -168,16 +174,33 @@ public data class ChannelMessageDetails( */ @Serializable public data class DirectMessageDetails( - override val id: String, - override val type: Int, - @SerialName("author_id") override val authorId: String, - override val content: String, - override val embeds: List> = emptyList(), - override val attachments: Attachments? = null, - override val reactions: List = emptyList(), - override val quote: Quote? = null, + public val id: String, + public val type: Int, + @SerialName("author_id") public val authorId: String, + public val content: String, + public val embeds: List> = emptyList(), + // 2024/8/6 + // 事件中,客户端发送的附件都被转成了card消息,attachments 永远为 null,看不出结构; + // 私聊事件查询详情时会得到空数组 `"attachments":[]`, 进而导致报错 + // 暂时不知道到底是两边都是数组,还是只有私聊变成了数组,因此暂且仅处理私聊。 + @SerialName("attachments") + public val attachmentsList: List? = null, + public val reactions: List = emptyList(), + // 如果没有引用,会冒出来一个空字符串。 + // 我真服了。 + @SerialName("quote") + private val sourceQuote: JsonElement? = null, @SerialName("read_status") public val readStatus: Boolean = false, -) : MessageDetails +) { + val quote: Quote? + get() { + val obj = (sourceQuote as? JsonObject?) ?: return null + return Kook.DEFAULT_JSON.decodeFromJsonElement(Quote.serializer(), obj) + } + + public val attachments: Attachments? + get() = attachmentsList?.firstOrNull() +} /** diff --git a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/Attachments.kt b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/Attachments.kt index 843f7858..72c75ec0 100644 --- a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/Attachments.kt +++ b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/Attachments.kt @@ -1,18 +1,21 @@ /* - * Copyright (c) 2023. ForteScarlet. + * Copyright (c) 2023-2024. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook 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-kook 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-kook 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. + * simbot-component-kook 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-kook, - * If not, see . + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see . */ package love.forte.simbot.kook.objects @@ -59,9 +62,9 @@ public interface Attachments { */ @Serializable public data class SimpleAttachments( - override val type: String, - override val url: String, - override val name: String, + override val type: String = "", + override val url: String = "", + override val name: String = "", override val size: Long = -1 ) : Attachments diff --git a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/card/Card.kt b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/card/Card.kt index ea268fb0..a5b00b9f 100644 --- a/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/card/Card.kt +++ b/simbot-component-kook-api/src/commonMain/kotlin/love/forte/simbot/kook/objects/card/Card.kt @@ -119,6 +119,7 @@ public data class CardMessage(private val cards: List) : List by car isLenient = true ignoreUnknownKeys = true encodeDefaults = true + coerceInputValues = true } /** diff --git a/simbot-component-kook-core/build.gradle.kts b/simbot-component-kook-core/build.gradle.kts index cadc3654..d4353203 100644 --- a/simbot-component-kook-core/build.gradle.kts +++ b/simbot-component-kook-core/build.gradle.kts @@ -18,6 +18,7 @@ * If not, see . */ +import com.google.devtools.ksp.gradle.KspTaskMetadata import love.forte.gradle.common.core.project.setup import love.forte.gradle.common.kotlin.multiplatform.applyTier1 import love.forte.gradle.common.kotlin.multiplatform.applyTier2 @@ -82,13 +83,11 @@ kotlin { } jvmTest.dependencies { - runtimeOnly(libs.ktor.client.cio) -// runtimeOnly(libs.kotlinx.coroutines.reactor) -// implementation(libs.reactor.core) + implementation(libs.ktor.client.java) - implementation(libs.log4j.api) - implementation(libs.log4j.core) - implementation(libs.log4j.slf4j2Impl) + implementation(libs.simbot.logger.slf4jimpl) +// implementation(libs.log4j.core) +// implementation(libs.log4j.slf4j2Impl) } jsMain.dependencies { @@ -109,6 +108,7 @@ kotlin { dependencies { add("kspJvm", project(":internal-processors:api-reader")) + add("kspCommonMainMetadata", project(":internal-processors:message-element-processor")) } ksp { @@ -117,3 +117,11 @@ ksp { arg("kook.api.finder.event.output", rootDir.resolve("generated-docs/core-event-list.md").absolutePath) arg("kook.api.finder.event.class", "love.forte.simbot.component.kook.event.KookEvent") } + + kotlin.sourceSets.commonMain { + // solves all implicit dependency trouble and IDEs source code detection + // see https://github.com/google/ksp/issues/963#issuecomment-1894144639 + tasks.withType { + kotlin.srcDir(destinationDirectory.file("kotlin")) + } + } diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookComponent.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookComponent.kt index ec07bd10..bf6984d8 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookComponent.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/KookComponent.kt @@ -21,11 +21,9 @@ package love.forte.simbot.component.kook -import kotlinx.serialization.modules.PolymorphicModuleBuilder import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass -import love.forte.simbot.annotations.ExperimentalSimbotAPI import love.forte.simbot.bot.serializableBotConfigurationPolymorphic import love.forte.simbot.common.function.ConfigurerFunction import love.forte.simbot.common.function.invokeBy @@ -34,11 +32,12 @@ import love.forte.simbot.component.ComponentConfigureContext import love.forte.simbot.component.ComponentFactory import love.forte.simbot.component.ComponentFactoryProvider import love.forte.simbot.component.kook.bot.KookBotVerifyInfoConfiguration -import love.forte.simbot.component.kook.message.* +import love.forte.simbot.component.kook.message.KookMessageElement +import love.forte.simbot.component.kook.message.includeKookMessageElements import love.forte.simbot.kook.ExperimentalKookApi import love.forte.simbot.kook.objects.kmd.KMarkdown import love.forte.simbot.kook.objects.kmd.RawValueKMarkdown -import love.forte.simbot.message.Message +import love.forte.simbot.message.messageElementPolymorphic import kotlin.jvm.JvmStatic /** @@ -104,36 +103,19 @@ public class KookComponent : Component { /** * [KookComponent] 组件所使用的消息序列化信息。 */ - @OptIn(ExperimentalSimbotAPI::class, ExperimentalKookApi::class) + @OptIn(ExperimentalKookApi::class) @get:JvmStatic public val messageSerializersModule: SerializersModule = SerializersModule { - fun PolymorphicModuleBuilder.include() { - subclass(KookAsset::class, KookAsset.serializer()) - subclass(KookAssetImage::class, KookAssetImage.serializer()) - subclass(KookAtAllHere::class, KookAtAllHere.serializer()) - // KookAttachmentMessage - subclass(KookAttachment::class, KookAttachment.serializer()) - subclass(KookAttachmentImage::class, KookAttachmentImage.serializer()) - subclass(KookAttachmentFile::class, KookAttachmentFile.serializer()) - subclass(KookAttachmentVideo::class, KookAttachmentVideo.serializer()) - - subclass(KookCardMessage::class, KookCardMessage.serializer()) - subclass(KookKMarkdownMessage::class, KookKMarkdownMessage.serializer()) - - subclass(KookTempTarget.Current::class, KookTempTarget.Current.serializer()) - subclass(KookTempTarget.Target::class, KookTempTarget.Target.serializer()) - } - - polymorphic(KMarkdown::class) { - subclass(RawValueKMarkdown::class, RawValueKMarkdown.serializer()) + polymorphic(KookMessageElement::class) { + includeKookMessageElements() } - polymorphic(KookMessageElement::class) { - include() + messageElementPolymorphic { + includeKookMessageElements() } - polymorphic(Message.Element::class) { - include() + polymorphic(KMarkdown::class) { + subclass(RawValueKMarkdown::class, RawValueKMarkdown.serializer()) } serializableBotConfigurationPolymorphic { diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/event/KookUpdatedMessageEvent.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/event/KookUpdatedMessageEvent.kt index 4c77b20c..7b9e1540 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/event/KookUpdatedMessageEvent.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/event/KookUpdatedMessageEvent.kt @@ -111,6 +111,7 @@ public abstract class KookUpdatedChannelMessageEvent : KookUpdatedMessageEvent() KookUpdatedMessageContent( bot = bot, isDirect = false, + chatCode = null, rawContent = sourceBody.content, msgId = sourceBody.msgId, mention = sourceBody.mention, @@ -178,6 +179,7 @@ public abstract class KookUpdatedPrivateMessageEvent : KookUpdatedMessageEvent() KookUpdatedMessageContent( bot = bot, isDirect = true, + chatCode = sourceBody.chatCode, rawContent = sourceBody.content, msgId = sourceBody.msgId, mention = emptyList(), diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookChannelMessageDetailsContent.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookChannelMessageDetailsContent.kt index 3c87c744..348b7ac7 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookChannelMessageDetailsContent.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookChannelMessageDetailsContent.kt @@ -31,6 +31,8 @@ import love.forte.simbot.common.id.ID import love.forte.simbot.common.id.StringID.Companion.ID import love.forte.simbot.component.kook.bot.KookBot import love.forte.simbot.component.kook.message.KookAttachmentMessage.Companion.asMessage +import love.forte.simbot.component.kook.message.KookQuote.Companion.asMessage +import love.forte.simbot.component.kook.util.requestDataBy import love.forte.simbot.component.kook.util.requestResultBy import love.forte.simbot.kook.api.ApiResponseException import love.forte.simbot.kook.api.message.DeleteChannelMessageApi @@ -43,6 +45,7 @@ import love.forte.simbot.message.Messages import love.forte.simbot.message.PlainText import love.forte.simbot.message.toText import kotlin.jvm.JvmStatic +import kotlin.jvm.JvmSynthetic /** * 将 [ChannelMessageDetails] 作为消息正文实现。 @@ -51,7 +54,7 @@ import kotlin.jvm.JvmStatic * @see GetChannelMessageViewApi * @author ForteScarlet */ -public data class KookChannelMessageDetailsContent( +public data class KookChannelMessageDetailsContent internal constructor( internal val details: ChannelMessageDetails, private val bot: KookBot, ) : KookMessageContent { @@ -91,6 +94,12 @@ public data class KookChannelMessageDetailsContent( messages.filterIsInstance().joinToString("") { it.text } } + @JvmSynthetic + override suspend fun reference(): KookQuote? { + val details = GetChannelMessageViewApi.create(details.id).requestDataBy(bot) + return details.quote?.asMessage() + } + /** * 删除当前的频道消息。 * diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookMessagesTransformer.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookMessagesTransformer.kt index fa14d3f0..3b751a6a 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookMessagesTransformer.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookMessagesTransformer.kt @@ -48,23 +48,17 @@ import love.forte.simbot.resource.ByteArrayResource import love.forte.simbot.resource.Resource -private fun createRequest( - type: Int, - content: String, - targetId: String, - quote: String?, - nonce: String?, - tempTargetId: String?, -): KookApi<*> { - return SendChannelMessageApi.create( - type = type, - targetId = targetId, - content = content, - quote = quote, - nonce = nonce, - tempTargetId = tempTargetId, - ) +private data class QuoteRef(var quote: String?) { + // 是否要让只有第一个消息有引用效果? +// fun take(): String? { +// val q = quote +// if (q != null) { +// quote = null +// } +// return q +// } } +private data class TempTargetIdRef(var tempTargetId: String?) private const val NOT_DIRECT = 0 private const val DIRECT_TYPE_BY_TARGET = 1 @@ -75,10 +69,13 @@ private const val DIRECT_TYPE_BY_CODE = 2 * 届时将会返回 [KookAggregatedMessageReceipt]. * * 消息的发送会更**倾向于**整合为一条或较少条消息,如果出现多条消息, - * 则 [quote] 会只被**第一条**消息所使用,而 [nonce] 和 [tempTargetId] 则会重复使用。 + * [quote]、[nonce] 和 [tempTargetId] 会被 **所有** 可能产生的消息重复使用。 * + * 其中,部分消息元素可能会覆盖默认值: + * - [MessageReference] 会覆盖 [quote] + * - [KookTempTarget] 会覆盖 [tempTargetId] * - * @return 消息最终的发送结果回执。如果为 null 则代表没有有效消息发送。 + * @return 消息最终地发送结果回执。如果为 `null` 则代表没有有效消息发送。 */ public suspend fun Message.sendToChannel( bot: KookBot, @@ -95,10 +92,13 @@ public suspend fun Message.sendToChannel( * 届时将会返回 [KookAggregatedMessageReceipt]. * * 消息的发送会更**倾向于**整合为一条或较少条消息,如果出现多条消息, - * 则 [quote] 会只被**第一条**消息所使用,而 [nonce] 和 [tempTargetId] 则会重复使用。 + * [quote]、[nonce] 和 [tempTargetId] 会被 **所有** 可能产生的消息重复使用。 * + * 其中,部分消息元素可能会覆盖默认值: + * - [MessageReference] 会覆盖 [quote] + * - [KookTempTarget] 会覆盖 [tempTargetId] * - * @return 消息最终的发送结果回执。如果为 null 则代表没有有效消息发送。 + * @return 消息最终地发送结果回执。如果为 `null` 则代表没有有效消息发送。 */ public suspend fun Message.sendToDirectByTargetId( bot: KookBot, @@ -113,10 +113,13 @@ public suspend fun Message.sendToDirectByTargetId( * 届时将会返回 [KookAggregatedMessageReceipt]. * * 消息的发送会更**倾向于**整合为一条或较少条消息,如果出现多条消息, - * 则 [quote] 会只被**第一条**消息所使用,而 [nonce] 和 [tempTargetId] 则会重复使用。 + * [quote]、[nonce] 和 [tempTargetId] 会被 **所有** 可能产生的消息重复使用。 * * + * 其中,部分消息元素可能会覆盖默认值: + * - [MessageReference] 会覆盖 [quote] + * - [KookTempTarget] 会覆盖 [tempTargetId] * - * @return 消息最终的发送结果回执。如果为 null 则代表没有有效消息发送。 + * @return 消息最终地发送结果回执。如果为 `null` 则代表没有有效消息发送。 */ public suspend fun Message.sendToDirectByChatCode( bot: KookBot, @@ -132,11 +135,15 @@ public suspend fun Message.sendToDirectByChatCode( * 届时将会返回 [KookAggregatedMessageReceipt]. * * 消息的发送会更**倾向于**整合为一条或较少条消息,如果出现多条消息, - * 则 [quote] 会只被**第一条**消息所使用,而 [nonce] 和 [tempTargetId] 则会重复使用。 + * [quote]、[nonce] 和 [tempTargetId] 会被 **所有** 可能产生的消息重复使用。 + * + * 其中,部分消息元素可能会覆盖默认值: + * - [MessageReference] 会覆盖 [quote] + * - [KookTempTarget] 会覆盖 [tempTargetId] * * @param defaultTempTargetId 如果存在 [KookTempTarget] * - * @return 消息最终的发送结果回执。如果为 null 则代表没有有效消息发送。 + * @return 消息最终地发送结果回执。如果为 `null` 则代表没有有效消息发送。 */ @OptIn(ExperimentalSimbotAPI::class) private suspend fun Message.send0( @@ -148,10 +155,6 @@ private suspend fun Message.send0( tempTargetId: String? = null, defaultTempTargetId: String? = null, ): KookMessageReceipt? { - data class TempTargetIdWrapper(var tempTargetId: String?) - -// var quote0 = quote - fun doRequest(type: Int, content: String, nonce: String?, quote: String?, tempTargetId: String?): KookApi<*> { return when (directType) { NOT_DIRECT -> SendChannelMessageApi.create( @@ -171,8 +174,8 @@ private suspend fun Message.send0( val message = this var kMarkdownBuilder: KMarkdownBuilder? = null - var quote0 = quote - val tempWrapper = TempTargetIdWrapper(tempTargetId) + val quoteRef = QuoteRef(quote) + val tempRef = TempTargetIdRef(tempTargetId) // val requests: List<KookApiRequest<*>> = buildList(if (this is Message.Element<*>) 1 else (this as Messages).size) { val requests: List<() -> KookApi<*>> = @@ -180,15 +183,13 @@ private suspend fun Message.send0( // 清算 kmd fun liquidationKmd() { kMarkdownBuilder?.let { kmb -> - val currentQuote = quote0 - quote0 = null add { doRequest( MessageType.KMARKDOWN.type, kmb.buildRaw(), nonce, - currentQuote, - tempWrapper.tempTargetId + quoteRef.quote, + tempRef.tempTargetId ) } kMarkdownBuilder = null @@ -203,14 +204,19 @@ private suspend fun Message.send0( } is KookTempTarget -> when (message) { - is KookTempTarget.Target -> tempWrapper.tempTargetId = message.id.literal - is KookTempTarget.Current -> tempWrapper.tempTargetId = defaultTempTargetId + is KookTempTarget.Target -> tempRef.tempTargetId = message.id.literal + is KookTempTarget.Current -> tempRef.tempTargetId = defaultTempTargetId + } + // 任意的 MessageReference 类型元素 + // 覆盖当前消息元素 + is MessageReference -> { + quoteRef.quote = message.id.literal } else -> { message.elementToRequest(bot, isSingle, { type, content -> liquidationKmd() - add { doRequest(type, content, nonce, null, tempWrapper.tempTargetId) } + add { doRequest(type, content, nonce, quoteRef.quote, tempRef.tempTargetId) } }) { block -> block(kMarkdownBuilder ?: KMarkdownBuilder().also { kMarkdownBuilder = it }) } @@ -320,10 +326,6 @@ private suspend inline fun Message.Element.elementToRequest( // card message is KookCardMessage -> doRequest(MessageType.CARD.type, message.cards.encode()) - // is KookRequestMessage -> { - // this.request - // } - is KookAtAllHere -> { withinKmd { at(AtTarget.Here) diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookQuote.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookQuote.kt new file mode 100644 index 00000000..0ed785ea --- /dev/null +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookQuote.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * This file is part of simbot-component-kook. + * + * simbot-component-kook 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-kook 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-kook, + * If not, see <https://www.gnu.org/licenses/>. + */ + +package love.forte.simbot.component.kook.message + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import love.forte.simbot.common.id.ID +import love.forte.simbot.common.id.StringID.Companion.ID +import love.forte.simbot.kook.objects.Quote +import love.forte.simbot.message.MessageIdReference +import love.forte.simbot.message.MessageReference +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + + +/** + * 一个通过 [KookMessageContent.reference] + * 查询得到的消息引用信息。 + * + * 也可用于发送,效果等同于使用 [MessageIdReference]。 + * + * @author ForteScarlet + */ +@SerialName("kook.quote") +@Serializable +public data class KookQuote internal constructor(public val quote: Quote) : + MessageReference { + override val id: ID + get() = quote.id.ID + + public companion object { + /** + * 将 [Quote] 作为 [KookQuote]。 + */ + @JvmStatic + @JvmName("create") + public fun Quote.asMessage(): KookQuote = KookQuote(this) + } + +} diff --git a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookReceiveMessageContent.kt b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookReceiveMessageContent.kt index a0e6db1e..af018dbb 100644 --- a/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookReceiveMessageContent.kt +++ b/simbot-component-kook-core/src/commonMain/kotlin/love/forte/simbot/component/kook/message/KookReceiveMessageContent.kt @@ -1,22 +1,26 @@ /* - * Copyright (c) 2022-2023. ForteScarlet. + * Copyright (c) 2022-2024. ForteScarlet. * - * This file is part of simbot-component-kook. + * This file is part of simbot-component-kook. * - * simbot-component-kook 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-kook 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-kook 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. + * simbot-component-kook 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-kook, - * If not, see <https://www.gnu.org/licenses/>. + * You should have received a copy of the GNU Lesser General Public License + * along with simbot-component-kook, + * If not, see <https://www.gnu.org/licenses/>. */ package love.forte.simbot.component.kook.message +import kotlinx.serialization.SerializationException import love.forte.simbot.ability.DeleteOption import love.forte.simbot.ability.DeleteSupport import love.forte.simbot.annotations.ExperimentalSimbotAPI @@ -28,16 +32,25 @@ import love.forte.simbot.component.kook.bot.KookBot import love.forte.simbot.component.kook.message.KookAttachmentMessage.Companion.asMessage import love.forte.simbot.component.kook.message.KookMessages.AT_TYPE_ROLE import love.forte.simbot.component.kook.message.KookMessages.AT_TYPE_USER +import love.forte.simbot.component.kook.message.KookQuote.Companion.asMessage +import love.forte.simbot.component.kook.util.requestData +import love.forte.simbot.component.kook.util.requestDataBy import love.forte.simbot.component.kook.util.requestResultBy import love.forte.simbot.component.kook.util.walk import love.forte.simbot.kook.api.ApiResponseException +import love.forte.simbot.kook.api.ApiResult +import love.forte.simbot.kook.api.ApiResultException import love.forte.simbot.kook.api.message.DeleteChannelMessageApi import love.forte.simbot.kook.api.message.DeleteDirectMessageApi +import love.forte.simbot.kook.api.message.GetChannelMessageViewApi +import love.forte.simbot.kook.api.message.GetDirectMessageViewApi +import love.forte.simbot.kook.api.userchat.CreateUserChatApi import love.forte.simbot.kook.event.* import love.forte.simbot.kook.objects.card.CardMessage import love.forte.simbot.logger.Logger import love.forte.simbot.logger.LoggerFactory import love.forte.simbot.message.* +import love.forte.simbot.suspendrunner.STP import kotlin.experimental.and import kotlin.experimental.or import kotlin.jvm.JvmSynthetic @@ -116,6 +129,31 @@ public interface KookMessageContent : MessageContent, DeleteSupport { */ override val messages: Messages + /** + * 获取消息中的引用信息。 + * 会通过 API [GetChannelMessageViewApi] + * 或 [GetDirectMessageViewApi] 发起请求并得到结果, + * 因此 [reference] 会产生挂起。 + * + * 如果是私聊会话,会先查询会话code,然后查询消息引用。 + * + * 查询结果不会被缓存,每次调用都会产生API请求。 + * + * @throws SerializationException 由于服务端响应值类型不规范导致的异常。 + * 2024/8/6: 经测试,在私聊会话情况下,如果发送的消息内无引用,则响应的 `quote` 值为 **空字符串**, + * 例如: + * ```json + * {"quote":""} + * ``` + * 这会引发 [SerializationException]。而如果消息包含引用,则响应为 `Quote` 对象结构。 + * 虽然在内部做了写兼容性的处理,但是不能保证未来服务端的行为不会发生变化。 + * + * @throws ApiResponseException 请求结果的状态码不是 200..300 之间 + * @throws ApiResultException 请求结果的 [ApiResult.code] 校验失败 + */ + @STP + override suspend fun reference(): KookQuote? + /** * 尝试根据当前消息ID删除目标。 * @@ -133,6 +171,22 @@ public interface KookMessageContent : MessageContent, DeleteSupport { public abstract class BaseKookReceiveMessageContent : KookMessageContent +private suspend fun referenceFromChannel(bot: KookBot, msgId: String): KookQuote? { + val api = GetChannelMessageViewApi.create(msgId) + val details = bot.requestData(api) + return details.quote?.asMessage() +} + +private suspend fun referenceFromDirect(bot: KookBot, msgId: String, authorId: String): KookQuote? { + val chat = CreateUserChatApi.create(authorId).requestDataBy(bot) + return referenceFromDirectWithChatCode(bot, msgId, chat.code) +} + +private suspend fun referenceFromDirectWithChatCode(bot: KookBot, msgId: String, chatCode: String): KookQuote? { + val details = GetDirectMessageViewApi.create(chatCode, msgId).requestDataBy(bot) + return details.quote?.asMessage() +} + /** * KOOK 消息事件所收到的消息正文类型。 * @@ -142,7 +196,7 @@ public abstract class BaseKookReceiveMessageContent : KookMessageContent * * @author ForteScarlet */ -public class KookReceiveMessageContent( +public class KookReceiveMessageContent internal constructor( private val isDirect: Boolean, internal val source: Event<TextExtra>, private val bot: KookBot, @@ -171,6 +225,15 @@ public class KookReceiveMessageContent( messages.filterIsInstance<PlainText>().joinToString("") { it.text } } + @JvmSynthetic + override suspend fun reference(): KookQuote? { + return if (isDirect) { + referenceFromDirect(bot, source.msgId, source.authorId) + } else { + referenceFromChannel(bot, source.msgId) + } + } + @JvmSynthetic override suspend fun delete(vararg options: DeleteOption) { // TODO options @@ -196,9 +259,10 @@ public class KookReceiveMessageContent( * * @see KookMessageContent */ -public class KookUpdatedMessageContent( +public class KookUpdatedMessageContent internal constructor( private val bot: KookBot, private val isDirect: Boolean, + private val chatCode: String? = null, override val rawContent: String, private val msgId: String, private val mention: List<String>, @@ -206,8 +270,13 @@ public class KookUpdatedMessageContent( private val isMentionAll: Boolean, private val isMentionHere: Boolean, ) : BaseKookReceiveMessageContent() { - override val id: ID get() = msgId.ID + init { + check(!isDirect || chatCode != null) { + "If is direct, chatCode can not be null" + } + } + override val id: ID get() = msgId.ID override val messages: Messages by lazy(LazyThreadSafetyMode.PUBLICATION) { toMessagesByKMarkdown(rawContent, mention, mentionRoles, isMentionAll, isMentionHere) @@ -217,6 +286,15 @@ public class KookUpdatedMessageContent( messages.filterIsInstance<PlainText>().joinToString("") { it.text } } + @JvmSynthetic + override suspend fun reference(): KookQuote? { + return if (isDirect) { + referenceFromDirectWithChatCode(bot, msgId, chatCode!!) + } else { + referenceFromChannel(bot, msgId) + } + } + @JvmSynthetic override suspend fun delete(vararg options: DeleteOption) { // TODO options