From e5cad1d0babe940ab91839ed6482ec974903cb73 Mon Sep 17 00:00:00 2001 From: Him188 <Him188@mamoe.net> Date: Wed, 20 Jul 2022 15:09:09 +0800 Subject: [PATCH] Rewrite MessageSerializers for new project structure (#2159) --- .../android/api/android.api | 7 +- .../compatibility-validation/jvm/api/jvm.api | 7 +- .../message/AbstractPolymorphicSerializer.kt | 121 +++++++ .../commonMain/kotlin/message/data/Image.kt | 45 ++- .../kotlin/message/data/MarketFace.kt | 2 + .../kotlin/message/data/MessageSource.kt | 14 +- .../kotlin/message/data/QuoteReply.kt | 3 +- .../kotlin/message/data/MarketFaceImpl.kt | 4 + .../message/protocol/impl/ImageProtocol.kt | 2 +- .../protocol/impl/MarketFaceProtocol.kt | 37 ++- .../protocol/impl/MusicShareProtocol.kt | 2 +- .../protocol/impl/QuoteReplyProtocol.kt | 34 +- .../serialization/MessageSerializer.kt | 6 +- .../message/source/incomingSourceImpl.kt | 10 +- .../message/source/offlineSourceImpl.kt | 4 +- .../message/source/outgoingSourceImpl.kt | 9 +- .../message/data/MessageSerializationTest.kt | 2 +- .../impl/AbstractMessageProtocolTest.kt | 302 ++++++++++++++++++ .../impl/CustomMessageProtocolTest.kt | 13 + .../message/protocol/impl/FaceProtocolTest.kt | 14 + .../protocol/impl/FileMessageProtocolTest.kt | 15 + .../protocol/impl/FlashImageProtocolTest.kt | Bin 5709 -> 6458 bytes .../impl/ForwardMessageProtocolTest.kt | 22 ++ .../protocol/impl/ImageProtocolTest.kt | 72 +++++ .../protocol/impl/LongMessageProtocolTest.kt | 2 + .../protocol/impl/MarketFaceProtocolTest.kt | 86 +++++ .../protocol/impl/MusicShareProtocolTest.kt | 25 ++ .../protocol/impl/PokeMessageProtocolTest.kt | 19 ++ .../protocol/impl/QuoteReplyProtocolTest.kt | 105 +++++- .../protocol/impl/RichMessageProtocolTest.kt | 69 ++++ .../message/protocol/impl/TextProtocolTest.kt | 50 +++ .../protocol/impl/VipFaceProtocolTest.kt | 23 ++ .../AbstractMessageSerializationTest.kt | 14 + 33 files changed, 1103 insertions(+), 37 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt create mode 100644 mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index e402a68a7..fa1dfa655 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -4815,8 +4815,13 @@ public final class net/mamoe/mirai/message/data/MessageSource$Key : net/mamoe/mi public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/MessageSource$Serializer : net/mamoe/mirai/internal/message/MessageSourceSerializerImpl { +public final class net/mamoe/mirai/message/data/MessageSource$Serializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSource$Serializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSource; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSource;)V } public final class net/mamoe/mirai/message/data/MessageSourceAmender : net/mamoe/mirai/message/data/MessageSourceBuilder { diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index ee7ca4faa..2acb48925 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -4815,8 +4815,13 @@ public final class net/mamoe/mirai/message/data/MessageSource$Key : net/mamoe/mi public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/MessageSource$Serializer : net/mamoe/mirai/internal/message/MessageSourceSerializerImpl { +public final class net/mamoe/mirai/message/data/MessageSource$Serializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSource$Serializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSource; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSource;)V } public final class net/mamoe/mirai/message/data/MessageSourceAmender : net/mamoe/mirai/message/data/MessageSourceBuilder { diff --git a/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt b/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt new file mode 100644 index 000000000..ea802b871 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.encoding.* +import net.mamoe.mirai.utils.cast +import kotlin.jvm.JvmName +import kotlin.reflect.KClass + +internal abstract class AbstractPolymorphicSerializer<T : Any> internal constructor() : KSerializer<T> { + + /** + * Base class for all classes that this polymorphic serializer can serialize or deserialize. + */ + abstract val baseClass: KClass<T> + + final override fun serialize(encoder: Encoder, value: T) { + val actualSerializer = findPolymorphicSerializer(encoder, value) + encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, actualSerializer.descriptor.serialName) + encodeSerializableElement(descriptor, 1, actualSerializer.cast(), value) + } + } + + final override fun deserialize(decoder: Decoder): T = decoder.decodeStructure(descriptor) { + var klassName: String? = null + var value: Any? = null + if (decodeSequentially()) { + return decodeSequentially(this) + } + + mainLoop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + CompositeDecoder.DECODE_DONE -> { + break@mainLoop + } + 0 -> { + klassName = decodeStringElement(descriptor, index) + } + 1 -> { + klassName = requireNotNull(klassName) { "Cannot read polymorphic value before its type token" } + val serializer = findPolymorphicSerializer(this, klassName) + value = decodeSerializableElement(descriptor, index, serializer) + } + else -> throw SerializationException( + "Invalid index in polymorphic deserialization of " + + (klassName ?: "unknown class") + + "\n Expected 0, 1 or DECODE_DONE(-1), but found $index" + ) + } + } + @Suppress("UNCHECKED_CAST") + requireNotNull(value) { "Polymorphic value has not been read for class $klassName" } as T + } + + private fun decodeSequentially(compositeDecoder: CompositeDecoder): T { + val klassName = compositeDecoder.decodeStringElement(descriptor, 0) + val serializer = findPolymorphicSerializer(compositeDecoder, klassName) + return compositeDecoder.decodeSerializableElement(descriptor, 1, serializer) + } + + /** + * Lookups an actual serializer for given [klassName] withing the current [base class][baseClass]. + * May use context from the [decoder]. + */ + open fun findPolymorphicSerializerOrNull( + decoder: CompositeDecoder, + klassName: String? + ): DeserializationStrategy<out T>? = decoder.serializersModule.getPolymorphic(baseClass, klassName) + + + /** + * Lookups an actual serializer for given [value] within the current [base class][baseClass]. + * May use context from the [encoder]. + */ + public open fun findPolymorphicSerializerOrNull( + encoder: Encoder, + value: T + ): SerializationStrategy<T>? = + encoder.serializersModule.getPolymorphic(baseClass, value) +} + + +internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer( + decoder: CompositeDecoder, + klassName: String? +): DeserializationStrategy<out T> = + findPolymorphicSerializerOrNull(decoder, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass) + +internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer( + encoder: Encoder, + value: T +): SerializationStrategy<T> = + findPolymorphicSerializerOrNull(encoder, value) ?: throwSubtypeNotRegistered(value::class, baseClass) + +@JvmName("throwSubtypeNotRegistered") +internal fun throwSubtypeNotRegistered(subClassName: String?, baseClass: KClass<*>): Nothing { + val scope = "in the scope of '${baseClass.simpleName}'" + throw SerializationException( + if (subClassName == null) + "Class discriminator was missing and no default polymorphic serializers were registered $scope" + else + "Class '$subClassName' is not registered for polymorphic serialization $scope.\n" + + "Mark the base class as 'sealed' or register the serializer explicitly." + ) +} + +@JvmName("throwSubtypeNotRegistered") +internal fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing = + throwSubtypeNotRegistered(subClass.simpleName ?: "$subClass", baseClass) diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt index 52061d3da..d2b15423f 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt @@ -23,7 +23,11 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.Bot import net.mamoe.mirai.IMirai @@ -111,6 +115,7 @@ import kotlin.native.CName * @see FlashImage 闪照 * @see Image.flash 转换普通图片为闪照 */ +@Suppress("DEPRECATION", "DEPRECATION_ERROR") @Serializable(Image.Serializer::class) @NotStableForInheritance public interface Image : Message, MessageContent, CodableMessage { @@ -183,17 +188,43 @@ public interface Image : Message, MessageContent, CodableMessage { deserialize = { Image(it) }, ) - public object Serializer : KSerializer<Image> by FallbackSerializer("Image") + @Deprecated( + message = "For internal use only. Deprecated for removal. Please retrieve serializer from MessageSerializers.serializersModule.", + level = DeprecationLevel.WARNING + ) + @DeprecatedSinceMirai(warningSince = "2.13") // error since 2.15, hidden since 2.16 + public object Serializer : KSerializer<Image> by FallbackSerializer(SERIAL_NAME) + // move to mirai-core in 2.16. Delegate Serializer to the implementation from MessageSerializers. @MiraiInternalApi - public open class FallbackSerializer(serialName: String) : KSerializer<Image> by Delegate.serializer().map( - buildClassSerialDescriptor(serialName) { element("imageId", String.serializer().descriptor) }, - serialize = { Delegate(imageId) }, - deserialize = { Image(imageId) }, - ) { + public open class FallbackSerializer(serialName: String) : KSerializer<Image> { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor(serialName) { + element("imageId", String.serializer().descriptor) + } + + // Note: Manually written to overcome discriminator issues. + // Without this implementation you will need `ignoreUnknownKeys` on deserialization. + override fun deserialize(decoder: Decoder): Image { + decoder.decodeStructure(descriptor) { + if (this.decodeSequentially()) { + val imageId = this.decodeStringElement(descriptor, 0) + return Image(imageId) + } else { + val index = this.decodeElementIndex(descriptor) + check(index == 0) + val imageId = this.decodeStringElement(descriptor, index) + return Image(imageId) + } + } + } + + override fun serialize(encoder: Encoder, value: Image) { + Delegate.serializer().serialize(encoder, Delegate(value.imageId)) + } + @SerialName(SERIAL_NAME) @Serializable - internal data class Delegate( + private data class Delegate( val imageId: String ) } diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt index 43c2a79f0..b4796ed6b 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/MarketFace.kt @@ -46,6 +46,8 @@ public interface MarketFace : HummerMessage { public companion object Key : AbstractPolymorphicMessageKey<HummerMessage, MarketFace>(HummerMessage, { it.safeCast() }) { + // Notice that for MarketFaceImpl, its serial name is 'MarketFace'; + // while for Dice, that is 'Dice' instead of 'MarketFace' again. (Dice extends MarketFace) public const val SERIAL_NAME: String = "MarketFace" } } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt index 6fd70f969..ad1199469 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt @@ -16,7 +16,9 @@ package net.mamoe.mirai.message.data import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonConfiguration import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.Bot import net.mamoe.mirai.IMirai @@ -25,10 +27,12 @@ import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.internal.message.MessageSourceSerializerImpl import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.action.AsyncRecallResult import net.mamoe.mirai.message.data.MessageSource.Key.quote import net.mamoe.mirai.message.data.MessageSource.Key.recall import net.mamoe.mirai.message.data.visitor.MessageVisitor +import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.safeCast @@ -111,6 +115,7 @@ import kotlin.jvm.JvmSynthetic * * @see buildMessageSource 构建一个 [OfflineMessageSource] */ +@Suppress("DEPRECATION") @Serializable(MessageSource.Serializer::class) public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle { public final override val key: MessageKey<MessageSource> @@ -198,9 +203,16 @@ public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle { return visitor.visitMessageSource(this, data) } - public object Serializer : MessageSourceSerializerImpl("MessageSource") + @Deprecated("Do not use this serializer. Retrieve from `MessageSerializers.serializersModule`.") + @DeprecatedSinceMirai(warningSince = "2.13") + public object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("MessageSource") public companion object Key : AbstractMessageKey<MessageSource>({ it.safeCast() }) { + /** + * 从 [MessageSerializers] 获取到的对应[序列化器][KSerializer]在参与多态序列化时的[类型标识符][JsonConfiguration.classDiscriminator]的值. + * + * [OnlineMessageSource] 的部分属性无法通过序列化保存. 所有 [MessageSource] 子类型在序列化时都会序列化为 [OfflineMessageSource]. 反序列化时会得到 [OfflineMessageSource] 而不是原类型. + */ public const val SERIAL_NAME: String = "MessageSource" /** diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt b/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt index 82fb97681..4357587aa 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt @@ -13,6 +13,7 @@ package net.mamoe.mirai.message.data +import kotlinx.serialization.Polymorphic import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.mamoe.mirai.message.data.MessageSource.Key.quote @@ -47,7 +48,7 @@ public data class QuoteReply( /** * 指代被引用的消息. 其中 [MessageSource.originalMessage] 可以控制客户端显示的消息内容. */ - public val source: MessageSource + public val source: @Polymorphic MessageSource ) : Message, MessageMetadata, ConstrainSingle { /** * 从消息链中获取 [MessageSource] 并构造. diff --git a/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt b/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt index ae7db7818..eb94988e1 100644 --- a/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/data/MarketFaceImpl.kt @@ -34,4 +34,8 @@ internal data class MarketFaceImpl internal constructor( override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R { return visitor.ex()?.visitMarketFaceImpl(this, data) ?: super.accept(visitor, data) } + + companion object { + const val SERIAL_NAME = MarketFace.SERIAL_NAME + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt index 929a73dfe..91c4cccea 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt @@ -40,11 +40,11 @@ internal class ImageProtocol : MessageProtocol() { add(ImagePatcherForGroup()) MessageSerializer.superclassesScope(MessageContent::class, SingleMessage::class) { + @Suppress("DEPRECATION", "DEPRECATION_ERROR") add(MessageSerializer(Image::class, Image.Serializer, registerAlsoContextual = true)) } MessageSerializer.superclassesScope(Image::class, MessageContent::class, SingleMessage::class) { - add(MessageSerializer(Image::class, Image.Serializer)) add(MessageSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())) add(MessageSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())) add(MessageSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer())) diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt index 99c01e273..4e914a5c8 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt @@ -20,7 +20,9 @@ import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext.Co import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.copy import net.mamoe.mirai.utils.hexToBytes +import net.mamoe.mirai.utils.map internal class MarketFaceProtocol : MessageProtocol() { @@ -30,16 +32,37 @@ internal class MarketFaceProtocol : MessageProtocol() { add(MarketFaceDecoder()) - MessageSerializer.superclassesScope(MarketFace::class, MessageContent::class, SingleMessage::class) { - add( - MessageSerializer( - MarketFaceImpl::class, - MarketFaceImpl.serializer() - ) + + // Serialization overview: + // Using MarketFace as serial type: + // - convert data to MarketFaceImpl on serialization. Convert them back to subtypes on deserialization. + // - serial name is always "MarketFace" + // Using subtypes: + // - serial name is name of subtype, i.e. "MarketFace" / "Dice". + // - Note that we don't use MarketFaceImpl but MarketFace for compatibility concerns. + + add( + MessageSerializer( + MarketFace::class, MarketFaceImpl.serializer().map( + resultantDescriptor = MarketFaceImpl.serializer().descriptor.copy(MarketFace.SERIAL_NAME), + deserialize = { + it.delegate.toDiceOrNull() ?: it + }, + serialize = { + when (it) { + is Dice -> MarketFaceImpl(it.toJceStruct()) + is MarketFaceImpl -> it + else -> { + error("Unsupported MarketFace type ${it::class.qualifiedName}") + } + } + } + ), emptyArray() ) - } + ) MessageSerializer.superclassesScope(MarketFace::class, MessageContent::class, SingleMessage::class) { + add(MessageSerializer(MarketFaceImpl::class, MarketFaceImpl.serializer())) add(MessageSerializer(Dice::class, Dice.serializer())) } } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt index 0198abec1..d655cd407 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt @@ -36,7 +36,7 @@ internal class MusicShareProtocol : MessageProtocol() { add(Sender()) MessageSerializer.superclassesScope(MessageContent::class, SingleMessage::class) { - add(MessageSerializer(MusicShare::class, MusicShare.serializer())) + add(MessageSerializer(MusicShare::class, MusicShare.serializer(), registerAlsoContextual = true)) } } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt index dbe99708e..8fcfd51db 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.AnonymousMember import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.internal.message.MessageSourceSerializerImpl import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.ProcessorCollector import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder @@ -26,6 +27,8 @@ import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer import net.mamoe.mirai.internal.message.source.* import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.copy +import net.mamoe.mirai.utils.map internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) { override fun ProcessorCollector.collectProcessorsImpl() { @@ -36,6 +39,7 @@ internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) { currentMessageChain[QuoteReply]?.source?.ensureSequenceIdAvailable() }) + val baseSourceSerializer = MessageSourceSerializerImpl(MessageSource.SERIAL_NAME) MessageSerializer.superclassesScope(MessageSource::class, MessageMetadata::class, SingleMessage::class) { add( MessageSerializer( @@ -92,9 +96,37 @@ internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) { ) ) - add(MessageSerializer(MessageSource::class, MessageSource.serializer())) } + MessageSerializer.superclassesScope(MessageMetadata::class, SingleMessage::class) { + @Suppress("DEPRECATION") + add( + MessageSerializer( + MessageSource::class, + OfflineMessageSourceImplData.serializer().map( + OfflineMessageSourceImplData.serializer().descriptor.copy(MessageSource.SERIAL_NAME), + { it }, + { + OfflineMessageSourceImplData( + kind, ids, botId, time, fromId, targetId, + originalMessage, internalIds + ) + } + ), + registerAlsoContextual = true + ) + ) + } + +// add( +// MessageSerializer( +// MessageSource::class, +// PolymorphicSerializer(MessageSource::class), +// emptyArray(), +// registerAlsoContextual = true +// ) +// ) + MessageSerializer.superclassesScope(MessageMetadata::class, SingleMessage::class) { add(MessageSerializer(QuoteReply::class, QuoteReply.serializer())) @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt b/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt index 02a8788d0..93b8a603e 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/serialization/MessageSerializer.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.message.protocol.serialization import kotlinx.serialization.KSerializer import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.ProcessorCollector import net.mamoe.mirai.message.data.SingleMessage import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -18,7 +19,8 @@ import kotlin.jvm.JvmInline import kotlin.reflect.KClass /** - * Collectd in [MessageProtocol.collectProcessors] + * Collectd in [MessageProtocol.collectProcessors]. + * @see ProcessorCollector.add */ internal class MessageSerializer<T : Any>( /** @@ -38,7 +40,7 @@ internal class MessageSerializer<T : Any>( // This can help native targets, which has no reflection support. companion object { - fun <T : SingleMessage, R> superclassesScope( + inline fun <T : SingleMessage, R> superclassesScope( vararg superclasses: KClass<in T>, block: SuperclassesScope<T>.() -> R ): R { diff --git a/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt index a34115f3b..fcf44b9fd 100644 --- a/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/source/incomingSourceImpl.kt @@ -13,6 +13,7 @@ package net.mamoe.mirai.internal.message.source import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import net.mamoe.mirai.Bot @@ -32,6 +33,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.structureToString import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.OnlineMessageSource import net.mamoe.mirai.message.data.visitor.MessageVisitor @@ -45,7 +47,7 @@ internal class OnlineMessageSourceFromFriendImpl( override val bot: Bot, msg: List<MsgComm.Msg>, ) : OnlineMessageSource.Incoming.FromFriend(), IncomingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromFriend") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromFriend") override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } override var isRecalledOrPlanned: AtomicBoolean = atomic(false) @@ -78,7 +80,7 @@ internal class OnlineMessageSourceFromStrangerImpl( override val bot: Bot, msg: List<MsgComm.Msg>, ) : OnlineMessageSource.Incoming.FromStranger(), IncomingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromStranger") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromStranger") override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } override var isRecalledOrPlanned: AtomicBoolean = atomic(false) @@ -150,7 +152,7 @@ internal class OnlineMessageSourceFromTempImpl( override val bot: Bot, msg: List<MsgComm.Msg>, ) : OnlineMessageSource.Incoming.FromTemp(), IncomingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromTemp") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromTemp") override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random } @@ -187,7 +189,7 @@ internal class OnlineMessageSourceFromGroupImpl( override val bot: Bot, msg: List<MsgComm.Msg>, ) : OnlineMessageSource.Incoming.FromGroup(), IncomingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl") @Transient override var isRecalledOrPlanned: AtomicBoolean = atomic(false) diff --git a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt index 8bd35d4b0..2e59f3876 100644 --- a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.message.source import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import net.mamoe.mirai.Bot @@ -23,6 +24,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.OfflineMessageSource import net.mamoe.mirai.message.data.visitor.MessageVisitor @@ -42,7 +44,7 @@ internal class OfflineMessageSourceImplData( private val originalMessageLazy: Lazy<MessageChain>, override val internalIds: IntArray, ) : OfflineMessageSource(), MessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OfflineMessageSource") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OfflineMessageSource") override val sequenceIds: IntArray get() = ids override val originalMessage: MessageChain by originalMessageLazy diff --git a/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt index 59054ca8c..6e0da7976 100644 --- a/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/source/outgoingSourceImpl.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.internal.message.source import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.atomic import kotlinx.coroutines.* +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* @@ -88,7 +89,7 @@ internal class OnlineMessageSourceToFriendImpl( override val sender: Bot, override val target: Friend, ) : OnlineMessageSource.Outgoing.ToFriend(), MessageSourceInternal, OutgoingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToFriend") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToFriend") override val isOriginalMessageInitialized: Boolean get() = true @@ -122,7 +123,7 @@ internal class OnlineMessageSourceToStrangerImpl( target: Stranger, ) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target) - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToStranger") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToStranger") override val isOriginalMessageInitialized: Boolean get() = true @@ -154,7 +155,7 @@ internal class OnlineMessageSourceToTempImpl( target: Member, ) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target) - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToTemp") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToTemp") override val isOriginalMessageInitialized: Boolean get() = true @@ -183,7 +184,7 @@ internal class OnlineMessageSourceToGroupImpl( override val target: Group, providedSequenceIds: IntArray? = null, ) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceInternal, OutgoingMessageSourceInternal { - object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToGroup") + object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("OnlineMessageSourceToGroup") override val isOriginalMessageInitialized: Boolean get() = true diff --git a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt index d05e2c93c..8656cf41e 100644 --- a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt @@ -195,7 +195,7 @@ internal class MessageSerializationTest : AbstractTest() { serializersModule = module ignoreUnknownKeys = true } - val source = j.decodeFromString(MessageSource.Serializer, a) + val source = j.decodeFromString(MessageSerializers.serializersModule.serializer<MessageSource>(), a) println(source.structureToString()) assertEquals( expected = Mirai.buildMessageSource(692928873, MessageSourceKind.GROUP) { diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt index 3321a6c41..361684a35 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt @@ -13,6 +13,14 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure +import kotlinx.serialization.json.* import net.mamoe.mirai.contact.ContactOrBot import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Group @@ -41,10 +49,14 @@ import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg import net.mamoe.mirai.internal.notice.processors.GroupExtensions import net.mamoe.mirai.internal.test.runBlockingUnit +import net.mamoe.mirai.internal.testFramework.dynamicTest +import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.message.data.MessageChain.Companion.serializeToJsonString import net.mamoe.mirai.utils.* import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import kotlin.reflect.KClass import kotlin.test.* @OptIn(TestOnly::class) @@ -352,4 +364,294 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler } } } + + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + open val format: Json + // `serializersModule` is volatile, always return new Json instances. + get() = Json { + prettyPrint = true + serializersModule = MessageSerializers.serializersModule + } + + /////////////////////////////////////////////////////////////////////////// + // serialization - polymorphism + /////////////////////////////////////////////////////////////////////////// + + interface PolymorphicWrapper { + val message: SingleMessage + } + + /** + * @param expectedSerialName also known as *poly discriminator*, give `null` to check for no discriminator's presence + */ + protected open fun <M : SingleMessage, P : PolymorphicWrapper> testPolymorphicIn( + polySerializer: KSerializer<P>, + polyConstructor: (message: M) -> P, + data: M, + expectedSerialName: String?, + expectedInstance: M = data, + ) { + val string = format.encodeToString( + polySerializer, + polyConstructor(data) + ) + println(string) + var element = format.parseToJsonElement(string) + element as JsonObject + element = element["message"] as JsonObject + if (expectedSerialName != null) { + assertEquals(expectedSerialName, element["type"]?.cast<JsonPrimitive>()?.content) + } else { + assertEquals(null, element["type"]) + } + assertEquals( + expectedInstance, + format.decodeFromString(polySerializer, string).message + ) + } + + @Serializable + data class PolymorphicWrapperSingleMessage( + override val message: @Polymorphic SingleMessage + ) : PolymorphicWrapper + + protected open fun <M : SingleMessage> testPolymorphicInSingleMessage( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInSingleMessage") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperSingleMessage.serializer(), + polyConstructor = ::PolymorphicWrapperSingleMessage, + data = data, + expectedSerialName = expectedSerialName, + expectedInstance = expectedInstance + ) + + }) + + @Serializable + data class PolymorphicWrapperMessageContent( + override val message: @Polymorphic MessageContent + ) : PolymorphicWrapper + + protected open fun <M : MessageContent> testPolymorphicInMessageContent( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInMessageContent") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperMessageContent.serializer(), + polyConstructor = ::PolymorphicWrapperMessageContent, + data = data, + expectedSerialName = expectedSerialName, + expectedInstance = expectedInstance + ) + }) + + @Serializable + data class PolymorphicWrapperMessageMetadata( + override val message: @Polymorphic MessageMetadata + ) : PolymorphicWrapper + + protected open fun <M : MessageMetadata> testPolymorphicInMessageMetadata( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInMessageMetadata") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperMessageMetadata.serializer(), + polyConstructor = ::PolymorphicWrapperMessageMetadata, + data = data, + expectedSerialName = expectedSerialName, + expectedInstance = expectedInstance + ) + }) + + /////////////////////////////////////////////////////////////////////////// + // serialization - in MessageChain + /////////////////////////////////////////////////////////////////////////// + + protected open fun <M : SingleMessage> testInsideMessageChain( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testInsideMessageChain") { + val chain = messageChainOf(data) + + val string = chain.serializeToJsonString(format) + println(string) + val element = format.parseToJsonElement(string).jsonArray.single().jsonObject + assertEquals(expectedSerialName, element["type"]?.cast<JsonPrimitive>()?.content) + + assertEquals( + expectedInstance, + MessageChain.deserializeFromJsonString(string).single() + ) + }) + + /////////////////////////////////////////////////////////////////////////// + // serialization - contextual + /////////////////////////////////////////////////////////////////////////// + + @Serializable + data class ContextualWrapper( + val message: @Contextual SingleMessage, + ) + + @Serializable + data class GenericTypedWrapper<T : SingleMessage>( + val message: T, + ) + + protected open fun <M : T, T : SingleMessage> testContextual( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + targetType: KClass<out T> = data::class, + ) = listOf( + testContextualWithWrapper<M, T>(data, expectedSerialName, expectedInstance), + testContextualWithStaticType(targetType, data, expectedInstance), + testContextualGeneric(targetType, data, expectedInstance), + testContextualWithoutWrapper<M, T>(data, expectedInstance) + ).flatten() + + private fun <M : T, T : SingleMessage> testContextualWithoutWrapper( + data: M, + expectedInstance: M + ) = listOf(dynamicTest("testContextualWithoutWrapper") { + @Suppress("UNCHECKED_CAST") + val serializer = ContextualSerializer(data::class) as KSerializer<M> + val string = format.encodeToString(serializer, data) + println(string) + val element = format.parseToJsonElement(string).jsonObject + assertEquals(null, element["type"]) + + assertEquals( + expectedInstance, + format.decodeFromString(serializer, string) + ) + }) + + private fun <M : T, T : SingleMessage> testContextualGeneric( + targetType: KClass<out T>, + data: M, + expectedInstance: M + ) = listOf(dynamicTest("testContextualGeneric") { + val messageSerializer: ContextualSerializer<SingleMessage> = ContextualSerializer(targetType).cast() + + /** + * ``` + * data class StaticTypedWrapper( + * val message: T + * ) + * ``` + * without concern of generic types. + */ + /** + * ``` + * data class StaticTypedWrapper( + * val message: T + * ) + * ``` + * without concern of generic types. + */ + val serializer = GenericTypedWrapper.serializer(messageSerializer) + + val string = format.encodeToString(serializer, GenericTypedWrapper(data)) + println(string) + val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject + + assertEquals(null, element["type"]?.jsonPrimitive?.content) + + assertEquals( + expectedInstance, + format.decodeFromString(serializer, string).message + ) + }) + + private fun <M : T, T : SingleMessage> testContextualWithStaticType( + targetType: KClass<out T>, + data: M, + expectedInstance: M, + ) = listOf(dynamicTest("testContextualWithStaticType") { + val messageSerializer: ContextualSerializer<SingleMessage> = ContextualSerializer(targetType).cast() + + /** + * ``` + * data class StaticTypedWrapper( + * val message: T + * ) + * ``` + * without concern of generic types. + */ + /** + * ``` + * data class StaticTypedWrapper( + * val message: T + * ) + * ``` + * without concern of generic types. + */ + val serializer = object : KSerializer<SingleMessage> { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StaticTypedWrapper") { + element("message", messageSerializer.descriptor, listOf(Contextual())) + } + + override fun deserialize(decoder: Decoder): SingleMessage { + decoder.decodeStructure(descriptor) { + if (this.decodeSequentially()) { + return this.decodeSerializableElement( + descriptor.getElementDescriptor(0), + 0, + messageSerializer + ) + } else { + val index = this.decodeElementIndex(descriptor) + check(index == 0) + return this.decodeSerializableElement(descriptor, index, messageSerializer) + } + } + } + + override fun serialize(encoder: Encoder, value: SingleMessage) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, messageSerializer, value) + } + } + } + + val string = format.encodeToString(serializer, data) + println(string) + val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject + + assertEquals(null, element["type"]?.jsonPrimitive?.content) + + assertEquals( + expectedInstance, + format.decodeFromString(serializer, string) + ) + }) + + private fun <M : T, T : SingleMessage> testContextualWithWrapper( + data: M, + expectedSerialName: String, + expectedInstance: M, + afterSerialization: (element: JsonObject) -> Unit = {} + ) = listOf(dynamicTest("testContextualWithWrapper") { + val string = format.encodeToString(ContextualWrapper.serializer(), ContextualWrapper(data)) + println(string) + val element = format.parseToJsonElement(string).jsonObject["message"]!!.jsonObject + afterSerialization(element) + + assertEquals(expectedSerialName, element["type"]?.jsonPrimitive?.content) + + assertEquals( + expectedInstance, + format.decodeFromString(ContextualWrapper.serializer(), string).message + ) + }) } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt index ef17a50e2..dc86c675f 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/CustomMessageProtocolTest.kt @@ -82,4 +82,17 @@ internal class CustomMessageProtocolTest : AbstractMessageProtocolTest() { message(MyCustomMessage(1)) }.doEncoderChecks() } + +// not supported, see https://github.com/mamoe/mirai/issues/2144 +// @TestFactory +// fun `test serialization`(): DynamicTestsResult { +// val data = MyCustomMessage(1) +// val serialName = "CustomMessage" +// return runDynamicTests( +// testPolymorphicInMessageMetadata(data, serialName), +// testPolymorphicInSingleMessage(data, serialName), +// testInsideMessageChain(data, serialName), +// testContextual(data), +// ) +// } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt index 385b20bda..2980b4ec8 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FaceProtocolTest.kt @@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.decodeAndRefineLight +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.Face import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.messageChainOf @@ -60,4 +63,15 @@ internal class FaceProtocolTest : AbstractMessageProtocolTest() { } + @TestFactory + fun `test serialization`(): DynamicTestsResult { + val data = Face(1) + val serialName = Face.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt index 438dc8346..23f9c8f58 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FileMessageProtocolTest.kt @@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest @@ -61,4 +64,16 @@ internal class FileMessageProtocolTest : AbstractMessageProtocolTest() { useOrdinaryEquality() }.doDecoderChecks() } + + @TestFactory + fun `test serialization`(): DynamicTestsResult { + val data = FileMessage("id", 1, "name", 2) + val serialName = FileMessage.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/FlashImageProtocolTest.kt index 20695daa0157c0d24299b533ad8842f42bce93f3..61e2d73397447c1e95bb9613a01e093fb9ee733d 100644 GIT binary patch delta 630 zcmX@Bv&(2h0Mq0NES%~ksl_F3MTxno<@rU~dM=fDiMg4{Awa?6pw!~hoD#0g+=Bd~ zlF9WfQVOVQL5kcGlS}f8Dv=bapeiaV%|kX5rtltz20MZ+$Lg7zn4Y@Xf=NVV@?2i7 zdYG{aK#*3Nr;q?LRG~PvC^Io9vnsJ9Ge0juL(@tD*=UG!6{@*Fnn0i|F-IXKu_RH! zR>3VNu{Z;4hX#aHs<t$6c6N0!bk;R;F*DIMHZ*q9HFYsF(={<OGpaXrc62f}bTO~h z%PL4$($qvT6XFa%V347htQYJW<mu=W@8{_23Rhp0T2fk+r+^j;8gO}#J3)R6$j_<F z%`Ym*$V~Rk^Q})UE(V5&bADb)YF>#3$P+qH--E2x)WNSmI5RIjC)F2X2r;@n^NKT5 eU>ck=5<@fdh|vaiUqwl2Vh%plkie_uss#Wi2g=_7 delta 14 VcmdmGbXI3W0Mq6q=0C!WwE!*b1&sgz diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt index 9b5875430..0f32d7ea2 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ForwardMessageProtocolTest.kt @@ -14,6 +14,9 @@ import net.mamoe.mirai.internal.message.LightMessageRefiner.dropMiraiInternalFla import net.mamoe.mirai.internal.message.data.ForwardMessageInternal import net.mamoe.mirai.internal.message.flags.IgnoreLengthCheck import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.cast import net.mamoe.mirai.utils.castUp @@ -143,4 +146,23 @@ internal class ForwardMessageProtocolTest : AbstractMessageProtocolTest() { // } // } // } + + @TestFactory + fun `test serialization`(): DynamicTestsResult { + val data = buildForwardMessage(defaultTarget.castUp()) { + add(1, "senderName", time = 123, message = PlainText("simple text")) + add(1, "senderName", time = 123) { + +PlainText("simple") + +Face(1) + +Image("{90CCED1C-2D64-313B-5D66-46625CAB31D7}.jpg") + } + } + val serialName = ForwardMessage.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt index 9648d45ee..d57bce8df 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/ImageProtocolTest.kt @@ -10,13 +10,22 @@ package net.mamoe.mirai.internal.message.protocol.impl import io.ktor.utils.io.core.* +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.Serializable import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.internal.message.image.OfflineFriendImage +import net.mamoe.mirai.internal.message.image.OfflineGroupImage import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.dynamicTest +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.ImageType import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertIs internal class ImageProtocolTest : AbstractMessageProtocolTest() { override val protocols: Array<out MessageProtocol> = arrayOf(ImageProtocol()) @@ -563,4 +572,67 @@ internal class ImageProtocolTest : AbstractMessageProtocolTest() { } + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + @Serializable + data class PolymorphicWrapperImage( + override val message: @Polymorphic Image + ) : PolymorphicWrapper + + private fun <M : Image> testPolymorphicInImage( + data: M, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInImage") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperImage.serializer(), + polyConstructor = ::PolymorphicWrapperImage, + data = data, + expectedSerialName = null, + expectedInstance = expectedInstance, + ) + }) + + @TestFactory + fun `test serialization for OfflineGroupImage`(): DynamicTestsResult { + val data = Image("{90CCED1C-2D64-313B-5D66-46625CAB31D7}.jpg") + assertIs<OfflineGroupImage>(data) + val serialName = Image.SERIAL_NAME + return runDynamicTests( + testPolymorphicInImage(data), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for OfflineFriendImage type 1`(): DynamicTestsResult { + val data = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f") // type 1 + assertIs<OfflineFriendImage>(data) + val serialName = Image.SERIAL_NAME + return runDynamicTests( + testPolymorphicInImage(data), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for OfflineFriendImage type 2`(): DynamicTestsResult { + val data = Image("/000000000-3814297509-BFB7027B9354B8F899A062061D74E206") // type 1 + assertIs<OfflineFriendImage>(data) + val serialName = Image.SERIAL_NAME + return runDynamicTests( + testPolymorphicInImage(data), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt index a5848fb2c..5010e119c 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/LongMessageProtocolTest.kt @@ -134,4 +134,6 @@ internal class LongMessageProtocolTest : AbstractMessageProtocolTest() { } } } + + // should add tests for refining received LongMessage to normal messages (with a MessageOrigin) } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt index 2078cbac2..c947c114e 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt @@ -10,10 +10,17 @@ package net.mamoe.mirai.internal.message.protocol.impl import io.ktor.utils.io.core.* +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.Serializable import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.data.MarketFaceImpl import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.dynamicTest +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.Dice +import net.mamoe.mirai.message.data.MarketFace import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest import kotlin.test.Test @@ -194,5 +201,84 @@ internal class MarketFaceProtocolTest : AbstractMessageProtocolTest() { }.doBothChecks() } + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + @Serializable + data class PolymorphicWrapperMarketFace( + override val message: @Polymorphic MarketFace + ) : PolymorphicWrapper + + @Serializable + data class StaticWrapperDice( + override val message: Dice + ) : PolymorphicWrapper + + private fun <M : MarketFace> testPolymorphicInMarketFace( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInMarketFace") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperMarketFace.serializer(), + polyConstructor = ::PolymorphicWrapperMarketFace, + data = data, + expectedSerialName = expectedSerialName, // MarketFaceImpl is 'MarketFace', Dice is 'Dice', should include discriminator + expectedInstance = expectedInstance, + ) + }) + + private fun testStaticDice( + data: Dice, + expectedInstance: Dice = data, + ) = listOf(dynamicTest("testStaticDice") { + testPolymorphicIn( + polySerializer = StaticWrapperDice.serializer(), + polyConstructor = ::StaticWrapperDice, + data = data, + expectedSerialName = null, + expectedInstance = expectedInstance, + ) + }) + + @TestFactory + fun `test serialization for MarketFaceImpl`(): DynamicTestsResult { + val data = MarketFaceImpl( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace( + faceName = "5B E5 8F 91 E5 91 86 5D".hexToBytes(), + itemType = 6, + faceInfo = 1, + faceId = "71 26 44 B5 27 94 46 11 99 8A EC 31 86 75 19 D2".hexToBytes(), + tabId = 10278, + subType = 3, + key = "726a53a5372b7289".toByteArray(), /* 37 32 36 61 35 33 61 35 33 37 32 62 37 32 38 39 */ + imageWidth = 200, + imageHeight = 200, + pbReserve = "0A 06 08 C8 01 10 C8 01 10 64 1A 0B 51 51 E5 A4 A7 E9 BB 84 E8 84 B8 22 40 68 74 74 70 73 3A 2F 2F 7A 62 2E 76 69 70 2E 71 71 2E 63 6F 6D 2F 69 70 3F 5F 77 76 3D 31 36 37 37 38 32 34 31 26 66 72 6F 6D 3D 61 69 6F 45 6D 6F 6A 69 4E 65 77 26 69 64 3D 31 30 38 39 31 30 2A 06 E6 9D A5 E8 87 AA 30 B5 BB B4 E3 0D 38 B5 BB B4 E3 0D 40 01 50 00".hexToBytes(), + ) + ) + val serialName = MarketFaceImpl.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMarketFace(data, serialName), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName, targetType = MarketFace::class), + ) + } + + @TestFactory + fun `test serialization for Dice`(): DynamicTestsResult { + val data = Dice(1) + val serialName = Dice.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMarketFace(data, serialName), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName, targetType = Dice::class), + testStaticDice(data), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt index 10654b6a0..52b4c64e5 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MusicShareProtocolTest.kt @@ -16,6 +16,9 @@ import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageProcessorAdapter import net.mamoe.mirai.internal.pipeline.replaceProcessor +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.LightApp import net.mamoe.mirai.message.data.MessageOrigin import net.mamoe.mirai.message.data.MessageOriginKind @@ -116,4 +119,26 @@ internal class MusicShareProtocolTest : AbstractMessageProtocolTest() { } } + @TestFactory + fun `test serialization for MusicShare`(): DynamicTestsResult { + val data = MusicShare( + kind = NeteaseCloudMusic, + title = "ジェリーフィッシュ", + summary = "Yunomi/ローラーガール", + jumpUrl = "https://y.music.163.com/m/song?id=562591636&uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&app_version=8.7.46", + pictureUrl = "http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg", + musicUrl = "http://music.163.com/song/media/outer/url?id=562591636&userid=324076307&sc=wmv&tn=", + brief = "[分享]ジェリーフィッシュ", + ) + + val serialName = MusicShare.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt index 311ac19c9..91d2b7543 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/PokeMessageProtocolTest.kt @@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.PokeMessage import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest @@ -82,4 +85,20 @@ internal class PokeMessageProtocolTest : AbstractMessageProtocolTest() { message(PokeMessage.ChuoYiChuo) }.doEncoderChecks() } + + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + @TestFactory + fun `test serialization for PokeMessage`(): DynamicTestsResult { + val data = PokeMessage.ChuoYiChuo + val serialName = PokeMessage.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt index be127ce14..49f0ce7c5 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt @@ -9,15 +9,19 @@ package net.mamoe.mirai.internal.message.protocol.impl +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.source.OfflineMessageSourceImplData import net.mamoe.mirai.internal.message.toMessageChainOnline +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.dynamicTest +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.internal.utils.runCoroutineInPlace -import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.MessageSerializers +import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.MessageSource.Key.quote -import net.mamoe.mirai.message.data.PlainText -import net.mamoe.mirai.message.data.QuoteReply -import net.mamoe.mirai.message.data.messageChainOf import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY import net.mamoe.mirai.utils.hexToBytes import kotlin.test.Test @@ -447,4 +451,97 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() { } + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + @TestFactory + fun `test serialization for QuoteReply`(): DynamicTestsResult { + val source = MessageSourceBuilder() + .sender(123) + .target(123) + .messages { + append("test") + } + .build(123, MessageSourceKind.FRIEND) + + val data = QuoteReply(source) + + val serialName = QuoteReply.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageMetadata(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + + /////////////////////////////////////////////////////////////////////////// + // MessageSource serialization + /////////////////////////////////////////////////////////////////////////// + + + // TODO: 2022/7/20 MessageSource 在 MessageMetadata 的 scope 多态序列化后会输出 'type' = 'MessageSource', 这是期望的行为. + // 但是在反序列化时会错误 unknown field 'type' + override val format: Json + get() = Json { + prettyPrint = true + serializersModule = MessageSerializers.serializersModule + ignoreUnknownKeys = true + } + + + @Serializable + data class PolymorphicWrapperMessageSource( + override val message: MessageSource + ) : PolymorphicWrapper + + private fun <M : MessageSource> testPolymorphicInMessageSource( + data: M, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInMessageSource") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperMessageSource.serializer(), + polyConstructor = ::PolymorphicWrapperMessageSource, + data = data, + expectedInstance = expectedInstance, + expectedSerialName = null, + ) + }) + + @TestFactory + fun `test serialization for OfflineMessageSource`(): DynamicTestsResult { + val data = MessageSourceBuilder() + .sender(123) + .target(123) + .messages { + append("test") + } + .build(123, MessageSourceKind.FRIEND) + + val serialName = MessageSource.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageSource(data), + testPolymorphicInMessageMetadata(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for OnlineMessageSource`(): DynamicTestsResult { + val data = onlineIncomingGroupMessage[MessageSource]!! + val expected = (data as OnlineMessageSource).toOffline() + + val serialName = MessageSource.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageSource(data, expectedInstance = expected), + testPolymorphicInMessageMetadata(data, serialName, expectedInstance = expected), + testPolymorphicInSingleMessage(data, serialName, expectedInstance = expected), + testInsideMessageChain(data, serialName, expectedInstance = expected), + testContextual(data, serialName, expectedInstance = expected), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt index e4305f6d6..360f4c03a 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/RichMessageProtocolTest.kt @@ -9,8 +9,15 @@ package net.mamoe.mirai.internal.message.protocol.impl +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.Serializable import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.dynamicTest +import net.mamoe.mirai.internal.testFramework.runDynamicTests +import net.mamoe.mirai.message.data.RichMessage import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest import kotlin.test.Test @@ -80,4 +87,66 @@ internal class RichMessageProtocolTest : AbstractMessageProtocolTest() { } // no encoder. specially handled, no test for now. + + + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + + @Serializable + data class PolymorphicWrapperRichMessage( + override val message: @Polymorphic RichMessage + ) : PolymorphicWrapper + + private fun <M : RichMessage> testPolymorphicInRichMessage( + data: M, + expectedSerialName: String, + expectedInstance: M = data, + ) = listOf(dynamicTest("testPolymorphicInRichMessage") { + testPolymorphicIn( + polySerializer = PolymorphicWrapperRichMessage.serializer(), + polyConstructor = ::PolymorphicWrapperRichMessage, + data = data, + expectedSerialName = expectedSerialName, + expectedInstance = expectedInstance + ) + }) + + @TestFactory + fun `test serialization for RichMessage`(): DynamicTestsResult { + val data = net.mamoe.mirai.message.data.SimpleServiceMessage( + serviceId = 1, + content = """ + <?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="1" templateID="123" action="" brief="[分享]ジェリーフィッシュ" sourceMsgId="0" url="https://y.music.163.com/m/song?id=562591636&uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&app_version=8.7.46" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2" advertiser_id="0" aid="0"><picture cover="http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg" w="0" h="0" /><title>ジェリーフィッシュ</title><summary>Yunomi/ローラーガール</summary></item><source name="网易云音乐" icon="https://i.gtimg.cn/open/app_icon/00/49/50/85/100495085_100_m.png" action="" a_actionData="tencent100495085://" appid="100495085" /></msg> + """.trimIndent() + ) + + val serialName = net.mamoe.mirai.message.data.SimpleServiceMessage.SERIAL_NAME + return runDynamicTests( + testPolymorphicInRichMessage(data, serialName), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for LightApp`(): DynamicTestsResult { + val data = net.mamoe.mirai.message.data.LightApp( + content = """ + <?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="1" templateID="123" action="" brief="[分享]ジェリーフィッシュ" sourceMsgId="0" url="https://y.music.163.com/m/song?id=562591636&uct=QK0IOc%2FSCIO8gBNG%2Bwcbsg%3D%3D&app_version=8.7.46" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2" advertiser_id="0" aid="0"><picture cover="http://p1.music.126.net/KaYSb9oYQzhl2XBeJcj8Rg==/109951165125601702.jpg" w="0" h="0" /><title>ジェリーフィッシュ</title><summary>Yunomi/ローラーガール</summary></item><source name="网易云音乐" icon="https://i.gtimg.cn/open/app_icon/00/49/50/85/100495085_100_m.png" action="" a_actionData="tencent100495085://" appid="100495085" /></msg> + """.trimIndent() + ) + + val serialName = net.mamoe.mirai.message.data.LightApp.SERIAL_NAME + return runDynamicTests( + testPolymorphicInRichMessage(data, serialName), + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt index a1a9d47c3..8ea3913bb 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/TextProtocolTest.kt @@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.AtAll import net.mamoe.mirai.message.data.PlainText @@ -115,4 +118,51 @@ internal class TextProtocolTest : AbstractMessageProtocolTest() { }) }.doBothChecks() } + + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + @TestFactory + fun `test serialization for PlainText`(): DynamicTestsResult { + val data = PlainText( + content = """foo""", + ) + + val serialName = PlainText.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for At`(): DynamicTestsResult { + val data = At( + 100 + ) + + val serialName = At.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + + @TestFactory + fun `test serialization for AtAll`(): DynamicTestsResult { + val data = AtAll + val serialName = AtAll.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt index b11b89346..018915ef2 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/VipFaceProtocolTest.kt @@ -11,6 +11,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.testFramework.DynamicTestsResult +import net.mamoe.mirai.internal.testFramework.TestFactory +import net.mamoe.mirai.internal.testFramework.runDynamicTests import net.mamoe.mirai.message.data.VipFace import net.mamoe.mirai.utils.hexToBytes import kotlin.test.BeforeTest @@ -48,4 +51,24 @@ internal class VipFaceProtocolTest : AbstractMessageProtocolTest() { useOrdinaryEquality() }.doDecoderChecks() } + + /////////////////////////////////////////////////////////////////////////// + // serialization + /////////////////////////////////////////////////////////////////////////// + + @TestFactory + fun `test serialization for VipFace`(): DynamicTestsResult { + val data = VipFace( + VipFace.LiuLian, 1 + ) + + val serialName = VipFace.SERIAL_NAME + return runDynamicTests( + testPolymorphicInMessageContent(data, serialName), + testPolymorphicInSingleMessage(data, serialName), + testInsideMessageChain(data, serialName), + testContextual(data, serialName), + ) + } + } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt b/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt new file mode 100644 index 000000000..aa23fd95d --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/serialization/AbstractMessageSerializationTest.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.serialization + +import net.mamoe.mirai.internal.test.AbstractTest + +internal abstract class AbstractMessageSerializationTest : AbstractTest() \ No newline at end of file