From a4b62b090932e3a1bc9b5316b755567488c6f80f Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Fri, 2 Jul 2021 12:10:20 +0800 Subject: [PATCH] Voice serialization (#1368) * Voice serialization * Implement `hashCode` and `equals` for `Voice` * Update test * Update test * Add note of `PttMessage.pttInternalInstance` * Fix logic of `Voice.equals` --- ...binary-compatibility-validator-android.api | 18 ++-- .../api/binary-compatibility-validator.api | 18 ++-- .../commonMain/kotlin/LowLevelApiAccessor.kt | 15 +++ .../commonMain/kotlin/message/data/Voice.kt | 98 ++++++++++++++++++- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 10 ++ .../message/data/MessageSerializationTest.kt | 38 +++++++ 6 files changed, 172 insertions(+), 25 deletions(-) diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index b79868e72..c0d95ff5d 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -5064,31 +5064,29 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion { public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage { public static final field Key Lnet/mamoe/mirai/message/data/Voice$Key; public static final field SERIAL_NAME Ljava/lang/String; - public synthetic fun (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public synthetic fun (Ljava/lang/String;[BJILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun contentToString ()Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z public final fun getCodec ()I public fun getFileName ()Ljava/lang/String; public fun getFileSize ()J public fun getMd5 ()[B public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer; - public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor; - public fun childSerializers ()[Lkotlinx/serialization/KSerializer; +public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class net/mamoe/mirai/message/data/Voice$Serializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$Serializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice; 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/Voice;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { - public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 588f0dcde..13ea6ff11 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -5064,31 +5064,29 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion { public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage { public static final field Key Lnet/mamoe/mirai/message/data/Voice$Key; public static final field SERIAL_NAME Ljava/lang/String; - public synthetic fun (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public synthetic fun (Ljava/lang/String;[BJILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun contentToString ()Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z public final fun getCodec ()I public fun getFileName ()Ljava/lang/String; public fun getFileSize ()J public fun getMd5 ()[B public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer; - public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor; - public fun childSerializers ()[Lkotlinx/serialization/KSerializer; +public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class net/mamoe/mirai/message/data/Voice$Serializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$Serializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice; 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/Voice;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { - public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { diff --git a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt index e9a3aace6..1cc3bff8c 100644 --- a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt +++ b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt @@ -15,8 +15,10 @@ import kotlinx.coroutines.Job import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.* +import net.mamoe.mirai.message.data.Voice import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.WeakRef import kotlin.annotation.AnnotationTarget.* @@ -293,4 +295,17 @@ public interface LowLevelApiAccessor { seconds: Int, ) + /** + * 序列化 [Voice.pttInternalInstance] + */ + @LowLevelApi + @MiraiInternalApi // For Voice serialize + public fun serializePttElem(ptt: Any?): String + + /** + * 反序列化 [Voice.pttInternalInstance] + */ + @LowLevelApi + @MiraiInternalApi // For Voice serialize + public fun deserializePttElem(ptt: String): Any? } diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt index 4406ff76a..e559e5128 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt @@ -9,15 +9,15 @@ package net.mamoe.mirai.message.data +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.internal.message.map +import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice -import net.mamoe.mirai.utils.MiraiExperimentalApi -import net.mamoe.mirai.utils.MiraiInternalApi -import net.mamoe.mirai.utils.safeCast /** @@ -41,9 +41,30 @@ public abstract class PttMessage : MessageContent { @MiraiExperimentalApi public abstract val fileSize: Long + /* + * **internal impl note** + * 用于中转 ImMsgBody.Ptt, 在接受到其他用户发送的语音时能按照原样发回, + * 并且便于未来修改 (对 api 修改最小化) + */ @MiraiInternalApi @Transient public var pttInternalInstance: Any? = null + set(value) { + field = value + _pttInternalInstanceSerializeCache = null + } + + @MiraiInternalApi + protected val pttInternalInstanceSerializeCache: String + get() { + _pttInternalInstanceSerializeCache?.let { return it } + return Mirai.serializePttElem(pttInternalInstance).also { + _pttInternalInstanceSerializeCache = it + } + } + + @Transient + private var _pttInternalInstanceSerializeCache: String? = null } /** @@ -58,7 +79,8 @@ public abstract class PttMessage : MessageContent { * * [Voice] 实例可以通过序列化方式保存. 下次可以用它发送因而不需要上传. 但可能由于未来服务器更新, 这项功能就不稳定. 因此建议总是上传音频文件而不要保存 [Voice]. */ -@Serializable // experimental +@Suppress("DuplicatedCode") +@Serializable(Voice.Serializer::class) // experimental @SerialName(Voice.SERIAL_NAME) public class Voice @MiraiInternalApi constructor( @MiraiExperimentalApi public override val fileName: String, @@ -92,4 +114,70 @@ public class Voice @MiraiInternalApi constructor( public override fun toString(): String = _stringValue!! public override fun contentToString(): String = "[语音消息]" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Voice) return false + + if (this.pttInternalInstance != null && other.pttInternalInstance != null) { + if (this.pttInternalInstance == other.pttInternalInstance) + return true + // strict + return this.pttInternalInstanceSerializeCache == other.pttInternalInstanceSerializeCache + } + + if (fileName != other.fileName) return false + if (!md5.contentEquals(other.md5)) return false + if (fileSize != other.fileSize) return false + if (codec != other.codec) return false + if (_url != other._url) return false + + return true + } + + override fun hashCode(): Int { + if (pttInternalInstance != null) + return pttInternalInstanceSerializeCache.hashCode() + + var result = fileName.hashCode() + result = 12 * result + md5.contentHashCode() + result = 54 * result + fileSize.hashCode() + result = 33 * result + codec + result = 15 * result + _url.hashCode() + return result + } + + public object Serializer : KSerializer by VoiceS.serializer().map( + resultantDescriptor = VoiceS.serializer().descriptor.copy(SERIAL_NAME), + deserialize = { + Voice( + fileName = it.fileName, + md5 = it.md5, + fileSize = it.fileSize, + codec = it.codec, + _url = it._url, + ).also { v -> v.pttInternalInstance = Mirai.deserializePttElem(it.ptt) } + }, + serialize = { + VoiceS( + fileName = it.fileName, + md5 = it.md5, + fileSize = it.fileSize, + _url = it._url, + codec = it.codec, + ptt = Mirai.serializePttElem(it.pttInternalInstance) + ) + } + ) { + @Serializable + @SerialName(SERIAL_NAME) + private class VoiceS( + val fileName: String, + val md5: ByteArray, + val fileSize: Long, + val codec: Int, + val _url: String, + val ptt: String = "", + ) + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 3f489a723..ce57832ad 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -1182,4 +1182,14 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { } } } + + override fun serializePttElem(ptt: Any?): String { + if (ptt !is ImMsgBody.Ptt) return "" + return ptt.toByteArray(ImMsgBody.Ptt.serializer()).toUHexString() + } + + override fun deserializePttElem(ptt: String): Any? { + if (ptt.isBlank()) return null + return ptt.hexToBytes().loadAs(ImMsgBody.Ptt.serializer()) + } } diff --git a/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt b/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt index 1a259ecb1..10dbc2aa7 100644 --- a/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt +++ b/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt @@ -217,4 +217,42 @@ internal class MessageSerializationTest { actual = source ) } + + @Serializable + data class V( + val msg: Voice + ) + + @Test + fun `test Voice serialization`() { + val v = V(Voice("4517", byteArrayOf(14), 50, 3, "https://github.com")) + println(v.serialize(V.serializer())) + assertEquals( + v.serialize(V.serializer()), + v.serialize(V.serializer()) + .deserialize(V.serializer()) + .serialize(V.serializer()) + ) + assertEquals( + v, + v.serialize(V.serializer()).deserialize(V.serializer()) + ) + v.msg.pttInternalInstance = ImMsgBody.Ptt( + srcUin = 1234567890, + fileMd5 = byteArrayOf(14, 81, 37, 14), + boolValid = true, + format = 90, + ) + println(v.serialize(V.serializer())) + assertEquals( + v.serialize(V.serializer()), + v.serialize(V.serializer()) + .deserialize(V.serializer()) + .serialize(V.serializer()) + ) + assertEquals( + v, + v.serialize(V.serializer()).deserialize(V.serializer()) + ) + } } \ No newline at end of file