From 8d8aca3f1cfabbd5c8b45c7f740fb6b443da005a Mon Sep 17 00:00:00 2001 From: Him188 <Him188@mamoe.net> Date: Thu, 5 Aug 2021 12:50:07 +0800 Subject: [PATCH] Introduce `Audio` for new API, revert changes on `Voice` --- ...binary-compatibility-validator-android.api | 122 +++++-- .../api/binary-compatibility-validator.api | 122 +++++-- .../commonMain/kotlin/LowLevelApiAccessor.kt | 16 - .../kotlin/contact/AudioSupported.kt | 45 +++ .../src/commonMain/kotlin/contact/Friend.kt | 2 +- .../src/commonMain/kotlin/contact/Group.kt | 14 +- .../kotlin/contact/VoiceSupported.kt | 39 --- .../message/MessageSerializersImpl.kt | 62 ++-- .../commonMain/kotlin/message/data/Audio.kt | 274 ++++++++++++++++ .../commonMain/kotlin/message/data/Voice.kt | 157 ++++----- .../kotlin/utils/ExternalResource.kt | 56 +--- .../src/commonMain/kotlin/Bytes.kt | 2 +- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 18 +- .../commonMain/kotlin/contact/FriendImpl.kt | 72 +++-- .../commonMain/kotlin/contact/GroupImpl.kt | 76 +++-- .../kotlin/message/OnlineAudioImpl.kt | 302 ++++++++++++++++++ .../kotlin/message/ReceiveMessageHandler.kt | 25 +- .../kotlin/network/highway/Highway.kt | 4 +- .../kotlin/network/protocol/data/proto/Msg.kt | 64 +++- .../chat/receive/MessageSvc.PbSendMsg.kt | 13 +- .../protocol/packet/chat/voice/PttStore.kt | 11 +- ...oe.mirai.message.data.OfflineAudio.Factory | 10 + .../commonTest/kotlin/message/AudioTest.kt | 75 +++++ .../message/data/MessageSerializationTest.kt | 112 +++++-- 24 files changed, 1313 insertions(+), 380 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/contact/AudioSupported.kt delete mode 100644 mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt create mode 100644 mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory create mode 100644 mirai-core/src/commonTest/kotlin/message/AudioTest.kt 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 ec33e5759..baacee8d3 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -176,6 +176,11 @@ public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/ma public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public abstract interface class net/mamoe/mirai/contact/AudioSupported : net/mamoe/mirai/contact/Contact { + public fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/OfflineAudio; + public abstract fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class net/mamoe/mirai/contact/BotIsBeingMutedException : java/lang/RuntimeException { public fun <init> (Lnet/mamoe/mirai/contact/Group;)V public final fun getTarget ()Lnet/mamoe/mirai/contact/Group; @@ -326,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; } -public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/VoiceSupported { +public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User { public fun delete ()V public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge; @@ -338,7 +343,7 @@ public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/corouti public abstract fun sendMessage (Lnet/mamoe/mirai/message/data/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported, net/mamoe/mirai/contact/VoiceSupported { +public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported { public static final field Companion Lnet/mamoe/mirai/contact/Group$Companion; public abstract fun contains (J)Z public fun contains (Lnet/mamoe/mirai/contact/NormalMember;)Z @@ -365,6 +370,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin public fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;)Z public abstract fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun setName (Ljava/lang/String;)V + public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice; + public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class net/mamoe/mirai/contact/Group$Companion { @@ -560,11 +567,6 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge; } -public abstract interface class net/mamoe/mirai/contact/VoiceSupported : net/mamoe/mirai/contact/Contact { - public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice; - public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - public abstract interface class net/mamoe/mirai/contact/announcement/Announcement { public static final field Companion Lnet/mamoe/mirai/contact/announcement/Announcement$Companion; public abstract fun getContent ()Ljava/lang/String; @@ -3158,6 +3160,41 @@ public final class net/mamoe/mirai/message/data/AtAll : net/mamoe/mirai/message/ public fun toString ()Ljava/lang/String; } +public abstract interface class net/mamoe/mirai/message/data/Audio : net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/Audio$Key; + public fun contentToString ()Ljava/lang/String; + public abstract fun getCodec ()Lnet/mamoe/mirai/message/data/AudioCodec; + public abstract fun getExtraData ()[B + public abstract fun getFileMd5 ()[B + public abstract fun getFileSize ()J + public abstract fun getFilename ()Ljava/lang/String; + public abstract fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/Audio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { +} + +public final class net/mamoe/mirai/message/data/AudioCodec : java/lang/Enum { + public static final field AMR Lnet/mamoe/mirai/message/data/AudioCodec; + public static final field Companion Lnet/mamoe/mirai/message/data/AudioCodec$Companion; + public static final field SILK Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun getFormatName ()Ljava/lang/String; + public final fun getId ()I + public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static fun values ()[Lnet/mamoe/mirai/message/data/AudioCodec; +} + +public final class net/mamoe/mirai/message/data/AudioCodec$Companion { + public final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec; +} + public abstract interface class net/mamoe/mirai/message/data/ConstrainSingle : net/mamoe/mirai/message/data/SingleMessage { public abstract fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; } @@ -4499,6 +4536,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils { public static final synthetic fun At (Lnet/mamoe/mirai/contact/UserOrBot;)Lnet/mamoe/mirai/message/data/At; public static final synthetic fun FileMessage (Ljava/lang/String;ILjava/lang/String;J)Lnet/mamoe/mirai/message/data/FileMessage; public static final synthetic fun Image (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; + public static final synthetic fun OfflineAudio (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public static final synthetic fun OfflineAudio (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; public static final synthetic fun UnsupportedMessage ([B)Lnet/mamoe/mirai/message/data/UnsupportedMessage; public static final synthetic fun at (Lnet/mamoe/mirai/contact/Member;)Lnet/mamoe/mirai/message/data/At; public static final fun buildMessageChain (ILkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/MessageChain; @@ -4541,6 +4580,7 @@ public final class net/mamoe/mirai/message/data/MessageUtils { public static final synthetic fun toMessageChain ([Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/data/MessageChain; public static final fun toOfflineMessageSource (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/OfflineMessageSource; public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText; + public static final synthetic fun toVoice (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; } public final class net/mamoe/mirai/message/data/MusicKind : java/lang/Enum { @@ -4604,6 +4644,26 @@ public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class net/mamoe/mirai/message/data/OfflineAudio : net/mamoe/mirai/message/data/Audio { + public static final field Key Lnet/mamoe/mirai/message/data/OfflineAudio$Key; + public static final field SERIAL_NAME Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/message/data/OfflineAudio$Factory { + public static final field INSTANCE Lnet/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE; + public abstract fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; +} + +public final class net/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE : net/mamoe/mirai/message/data/OfflineAudio$Factory { + public fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; +} + +public final class net/mamoe/mirai/message/data/OfflineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/mamoe/mirai/message/data/MessageSource { public static final field Key Lnet/mamoe/mirai/message/data/OfflineMessageSource$Key; public fun <init> ()V @@ -4613,6 +4673,17 @@ public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/ma public final class net/mamoe/mirai/message/data/OfflineMessageSource$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/OnlineAudio : net/mamoe/mirai/message/data/Audio { + public static final field Key Lnet/mamoe/mirai/message/data/OnlineAudio$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public abstract fun getLength ()J + public abstract fun getUrlForDownload ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OnlineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mamoe/mirai/message/data/MessageSource { public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Key; public abstract fun getBot ()Lnet/mamoe/mirai/Bot; @@ -5090,32 +5161,39 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage { +public 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 <init> (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public synthetic fun <init> (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 static final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; public fun getFileName ()Ljava/lang/String; public fun getFileSize ()J public fun getMd5 ()[B - public final fun getUrl ()Ljava/lang/String; + public fun getUrl ()Ljava/lang/String; + public final fun get_codec ()I public fun hashCode ()I + public final fun toAudio ()Lnet/mamoe/mirai/message/data/Audio; public fun toString ()Ljava/lang/String; } -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 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 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 fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { @@ -5429,14 +5507,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static fun uploadAsImage (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage; @@ -5504,14 +5576,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/Image; public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage; diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 992a66c04..01f874110 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -176,6 +176,11 @@ public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/ma public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public abstract interface class net/mamoe/mirai/contact/AudioSupported : net/mamoe/mirai/contact/Contact { + public fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/OfflineAudio; + public abstract fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class net/mamoe/mirai/contact/BotIsBeingMutedException : java/lang/RuntimeException { public fun <init> (Lnet/mamoe/mirai/contact/Group;)V public final fun getTarget ()Lnet/mamoe/mirai/contact/Group; @@ -326,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; } -public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/VoiceSupported { +public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User { public fun delete ()V public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge; @@ -338,7 +343,7 @@ public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/corouti public abstract fun sendMessage (Lnet/mamoe/mirai/message/data/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported, net/mamoe/mirai/contact/VoiceSupported { +public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported { public static final field Companion Lnet/mamoe/mirai/contact/Group$Companion; public abstract fun contains (J)Z public fun contains (Lnet/mamoe/mirai/contact/NormalMember;)Z @@ -365,6 +370,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin public fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;)Z public abstract fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun setName (Ljava/lang/String;)V + public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice; + public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class net/mamoe/mirai/contact/Group$Companion { @@ -560,11 +567,6 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge; } -public abstract interface class net/mamoe/mirai/contact/VoiceSupported : net/mamoe/mirai/contact/Contact { - public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice; - public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - public abstract interface class net/mamoe/mirai/contact/announcement/Announcement { public static final field Companion Lnet/mamoe/mirai/contact/announcement/Announcement$Companion; public abstract fun getContent ()Ljava/lang/String; @@ -3158,6 +3160,41 @@ public final class net/mamoe/mirai/message/data/AtAll : net/mamoe/mirai/message/ public fun toString ()Ljava/lang/String; } +public abstract interface class net/mamoe/mirai/message/data/Audio : net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/Audio$Key; + public fun contentToString ()Ljava/lang/String; + public abstract fun getCodec ()Lnet/mamoe/mirai/message/data/AudioCodec; + public abstract fun getExtraData ()[B + public abstract fun getFileMd5 ()[B + public abstract fun getFileSize ()J + public abstract fun getFilename ()Ljava/lang/String; + public abstract fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/Audio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { +} + +public final class net/mamoe/mirai/message/data/AudioCodec : java/lang/Enum { + public static final field AMR Lnet/mamoe/mirai/message/data/AudioCodec; + public static final field Companion Lnet/mamoe/mirai/message/data/AudioCodec$Companion; + public static final field SILK Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public static final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun getFormatName ()Ljava/lang/String; + public final fun getId ()I + public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public static fun values ()[Lnet/mamoe/mirai/message/data/AudioCodec; +} + +public final class net/mamoe/mirai/message/data/AudioCodec$Companion { + public final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec; + public final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec; +} + public abstract interface class net/mamoe/mirai/message/data/ConstrainSingle : net/mamoe/mirai/message/data/SingleMessage { public abstract fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; } @@ -4499,6 +4536,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils { public static final synthetic fun At (Lnet/mamoe/mirai/contact/UserOrBot;)Lnet/mamoe/mirai/message/data/At; public static final synthetic fun FileMessage (Ljava/lang/String;ILjava/lang/String;J)Lnet/mamoe/mirai/message/data/FileMessage; public static final synthetic fun Image (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; + public static final synthetic fun OfflineAudio (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public static final synthetic fun OfflineAudio (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; public static final synthetic fun UnsupportedMessage ([B)Lnet/mamoe/mirai/message/data/UnsupportedMessage; public static final synthetic fun at (Lnet/mamoe/mirai/contact/Member;)Lnet/mamoe/mirai/message/data/At; public static final fun buildMessageChain (ILkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/MessageChain; @@ -4541,6 +4580,7 @@ public final class net/mamoe/mirai/message/data/MessageUtils { public static final synthetic fun toMessageChain ([Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/data/MessageChain; public static final fun toOfflineMessageSource (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/OfflineMessageSource; public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText; + public static final synthetic fun toVoice (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; } public final class net/mamoe/mirai/message/data/MusicKind : java/lang/Enum { @@ -4604,6 +4644,26 @@ public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class net/mamoe/mirai/message/data/OfflineAudio : net/mamoe/mirai/message/data/Audio { + public static final field Key Lnet/mamoe/mirai/message/data/OfflineAudio$Key; + public static final field SERIAL_NAME Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/message/data/OfflineAudio$Factory { + public static final field INSTANCE Lnet/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE; + public abstract fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; +} + +public final class net/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE : net/mamoe/mirai/message/data/OfflineAudio$Factory { + public fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio; + public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio; +} + +public final class net/mamoe/mirai/message/data/OfflineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/mamoe/mirai/message/data/MessageSource { public static final field Key Lnet/mamoe/mirai/message/data/OfflineMessageSource$Key; public fun <init> ()V @@ -4613,6 +4673,17 @@ public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/ma public final class net/mamoe/mirai/message/data/OfflineMessageSource$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/OnlineAudio : net/mamoe/mirai/message/data/Audio { + public static final field Key Lnet/mamoe/mirai/message/data/OnlineAudio$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public abstract fun getLength ()J + public abstract fun getUrlForDownload ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OnlineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mamoe/mirai/message/data/MessageSource { public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Key; public abstract fun getBot ()Lnet/mamoe/mirai/Bot; @@ -5090,32 +5161,39 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage { +public 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 <init> (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public synthetic fun <init> (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 static final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; public fun getFileName ()Ljava/lang/String; public fun getFileSize ()J public fun getMd5 ()[B - public final fun getUrl ()Ljava/lang/String; + public fun getUrl ()Ljava/lang/String; + public final fun get_codec ()I public fun hashCode ()I + public final fun toAudio ()Lnet/mamoe/mirai/message/data/Audio; public fun toString ()Ljava/lang/String; } -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 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 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 fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice; + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { @@ -5429,14 +5507,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static fun uploadAsImage (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage; @@ -5504,14 +5576,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/Image; public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice; - public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage; diff --git a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt index aee96bace..0f344257d 100644 --- a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt +++ b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt @@ -15,9 +15,7 @@ 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.MiraiExperimentalApi -import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.WeakRef import kotlin.annotation.AnnotationTarget.* @@ -219,18 +217,4 @@ public interface LowLevelApiAccessor { groupId: Long, 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/contact/AudioSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/AudioSupported.kt new file mode 100644 index 000000000..c6c9eadd9 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/AudioSupported.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2021 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 + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.contact + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.message.data.Audio +import net.mamoe.mirai.message.data.OfflineAudio +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.OverFileSizeMaxException + +/** + * 支持发送语音的 [Contact] + * + * @since 2.7 + */ +@NotStableForInheritance +public interface AudioSupported : Contact { + /** + * 上传一个语音文件以备发送. [resource] 需要调用方[关闭][ExternalResource.close]. + * + * 多次调用 [uploadAudio] 使用同一个 [resource] 时, 将会发生多次上传, 且有可能产生不同的 [OfflineAudio] 对象, 因为服务器不会提供有关文件是否已经存在于服务器的信息. + * + * 返回的 [OfflineAudio] 支持序列化, 可以保存后在将来使用, 而不需要立即[发送][Contact.sendMessage]. 但不建议保存太久, 无法确定服务器保留一个文件的时间. + * + * 建议使用同一个 [Contact] 进行 [uploadAudio] 和 [sendMessage]. 目标对象不同时的行为是不确定的. + * + * 要获取更多语音相关的信息, 参阅 [Audio]. + * + * @throws OverFileSizeMaxException 当语音文件过大而被服务器拒绝上传时. (最大大小约为 1 MB) + * **注意**: 由于服务器不一定会检查大小, 该异常就不一定会因大小超过 1MB 而抛出. + * + * @since 2.7 + */ + public suspend fun uploadAudio(resource: ExternalResource): OfflineAudio +} diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Friend.kt b/mirai-core-api/src/commonMain/kotlin/contact/Friend.kt index 5014c0340..bbb6486ba 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Friend.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Friend.kt @@ -34,7 +34,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance * @see FriendMessageEvent */ @NotStableForInheritance -public interface Friend : User, CoroutineScope, VoiceSupported { +public interface Friend : User, CoroutineScope, AudioSupported { /** * 向这个对象发送消息. * diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt index b539ab810..7de30e23b 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt @@ -19,6 +19,7 @@ import net.mamoe.mirai.contact.announcement.Announcements import net.mamoe.mirai.event.events.* import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.NotStableForInheritance @@ -52,7 +53,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance * ## */ @NotStableForInheritance -public interface Group : Contact, CoroutineScope, FileSupported, VoiceSupported { +public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported { /** * 群名称. * @@ -184,6 +185,17 @@ public interface Group : Contact, CoroutineScope, FileSupported, VoiceSupported this.sendMessage(message.toPlainText()) + /** + * 上传一个语音消息以备发送. 该方法已弃用且将在未来版本删除, 请使用 [uploadAudio]. + */ + @Suppress("DEPRECATION") + @Deprecated( + "use uploadAudio", + replaceWith = ReplaceWith("uploadAudio(resource)"), + level = DeprecationLevel.WARNING + ) + public suspend fun uploadVoice(resource: ExternalResource): Voice + /** * 将一条消息设置为群精华消息, 需要管理员或群主权限. * 操作成功返回 `true`. diff --git a/mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt deleted file mode 100644 index 5db799a70..000000000 --- a/mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2019-2021 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.contact - -import net.mamoe.kjbb.JvmBlockingBridge -import net.mamoe.mirai.message.data.Voice -import net.mamoe.mirai.utils.ExternalResource -import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice -import net.mamoe.mirai.utils.NotStableForInheritance -import net.mamoe.mirai.utils.OverFileSizeMaxException - -/** - * 支持发送语音的 [Contact] - * - * @since 2.7 - */ -@NotStableForInheritance -public interface VoiceSupported : Contact { - /** - * 上传一个语音消息以备发送. - * - * - **请手动关闭 [resource]** - * - 请使用 amr 或 silk 格式 - * - * @since 2.7 - * @see ExternalResource.uploadAsVoice - * @throws OverFileSizeMaxException 当语音文件过大而被服务器拒绝上传时. (最大大小约为 1 MB) - */ - @JvmBlockingBridge - public suspend fun uploadVoice(resource: ExternalResource): Voice - -} diff --git a/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt b/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt index 232512fdf..0cce9fa96 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt @@ -13,14 +13,12 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.modules.PolymorphicModuleBuilder -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.overwriteWith -import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.* import net.mamoe.mirai.Mirai import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.lateinitMutableProperty import net.mamoe.mirai.utils.map import net.mamoe.mirai.utils.takeElementsFrom import kotlin.reflect.KClass @@ -121,6 +119,7 @@ private val builtInSerializersModule by lazy { subclass(SimpleServiceMessage::class, SimpleServiceMessage.serializer()) // subclass(PttMessage::class, PttMessage.serializer()) + @Suppress("DEPRECATION") subclass(Voice::class, Voice.serializer()) // subclass(HummerMessage::class, HummerMessage.serializer()) @@ -188,31 +187,58 @@ private val builtInSerializersModule by lazy { // Tests: // net.mamoe.mirai.internal.message.data.MessageSerializationTest internal object MessageSerializersImpl : MessageSerializers { - @Volatile - private var serializersModuleField: SerializersModule? = null + private var serializersModuleField: SerializersModule by lateinitMutableProperty { + builtInSerializersModule + } + override val serializersModule: SerializersModule get() { Mirai // ensure registered, for tests - return serializersModuleField ?: builtInSerializersModule + return serializersModuleField } @Synchronized override fun <M : SingleMessage> registerSerializer(type: KClass<M>, serializer: KSerializer<M>) { - serializersModuleField = serializersModule.overwriteWith(SerializersModule { - // contextual(type, serializer) - for (superclass in type.allSuperclasses) { - if (superclass.isFinal) continue - if (!superclass.isSubclassOf(SingleMessage::class)) continue - @Suppress("UNCHECKED_CAST") - polymorphic(superclass as KClass<Any>) { - subclass(type, serializer) - } - } - }) + serializersModuleField = serializersModule.overwritePolymorphicWith(type, serializer) } @Synchronized override fun registerSerializers(serializersModule: SerializersModule) { serializersModuleField = serializersModule.overwriteWith(serializersModule) } +} + +internal fun <M : Any> SerializersModule.overwritePolymorphicWith( + type: KClass<M>, + serializer: KSerializer<M> +): SerializersModule { + return overwriteWith(SerializersModule { + // contextual(type, serializer) + for (superclass in type.allSuperclasses) { + if (superclass.isFinal) continue + if (!superclass.isSubclassOf(SingleMessage::class)) continue + @Suppress("UNCHECKED_CAST") + polymorphic(superclass as KClass<Any>) { + subclass(type, serializer) + } + } + }) +} + +private inline fun <reified M : SingleMessage> SerializersModuleBuilder.hierarchicallyPolymorphic(serializer: KSerializer<M>) = + hierarchicallyPolymorphic(M::class, serializer) + +private fun <M : SingleMessage> SerializersModuleBuilder.hierarchicallyPolymorphic( + type: KClass<M>, + serializer: KSerializer<M> +) { + // contextual(type, serializer) + for (superclass in type.allSuperclasses) { + if (superclass.isFinal) continue + if (!superclass.isSubclassOf(SingleMessage::class)) continue + @Suppress("UNCHECKED_CAST") + polymorphic(superclass as KClass<Any>) { + subclass(type, serializer) + } + } } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt new file mode 100644 index 000000000..e71e6543e --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt @@ -0,0 +1,274 @@ +/* + * Copyright 2019-2021 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 + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") +@file:JvmMultifileClass +@file:JvmName("MessageUtils") + +package net.mamoe.mirai.message.data + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import net.mamoe.mirai.contact.AudioSupported +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.message.MessageSerializers +import net.mamoe.mirai.message.data.MessageChain.Companion.serializeToJsonString +import net.mamoe.mirai.utils.* + +/** + * 语音消息. + * + * [Audio] 分为 [OnlineAudio] 与 [OfflineAudio]. 在本地上传的, 或手动构造的语音为 [OfflineAudio]. 从服务器接收的语音为 [OnlineAudio]. + * + * ## 上传和发送语音 + * + * 使用 [AudioSupported.uploadAudio] 上传语音到服务器并取得 [Audio] 消息实例, 然后通过 [Contact.sendMessage] 发送. + * + * Java 示例: + * ``` + * Audio audio; + * try { + * audio = group.uploadAudio(resource); // 上传文件得到语音实例 + * } finally { + * resource.close(); // 保证资源正常关闭 + * } + * group.sendMessage(audio); // 发送语音消息 + * ``` + * + * ## 下载语音 + * + * 使用 [OnlineAudio.urlForDownload] 获取文件下载链接. + * + * ## [Audio] 与 [Voice] 的转换 + * + * 原 [Voice] 已弃用故不推荐进行兼容转换. [Audio] 将有稳定性保证, 请尽量使用新的 [Audio]. + * + * 将 [Audio] 转为 [Voice]: [Voice.fromAudio] + * 将 [Voice] 转为 [Audio]: [Voice.toAudio] + * + * @since 2.7 + */ +public sealed interface Audio : MessageContent { + public companion object Key : + AbstractPolymorphicMessageKey<MessageContent, Audio>(MessageContent, { it.safeCast() }) + + /** + * 文件名称. 通常为 `XXX.amr`. 服务器要求文件名后缀必须为 ".amr", 但其[编码方式][codec]也有可能是非 [AudioCodec.AMR]. + */ + public val filename: String + + /** + * 文件 MD5. 16 bytes. + */ + public val fileMd5: ByteArray + + /** + * 文件大小 bytes. 官方客户端支持最大文件大小约为 1MB, 过大的文件**可能**可以正常上传, 但在官方客户端无法收听 (显示文件损坏). + */ + public val fileSize: Long + + /** + * 编码方式. + * + * - 若语音文件真实编码方式为 [AudioCodec.SILK], 而该属性为 [AudioCodec.AMR], 语音文件将会被服务器压缩为低音质 [AudioCodec.AMR] 格式. + * - 若语音文件真实编码方式为 [AudioCodec.AMR], 而该属性为 [AudioCodec.SILK], 语音也可以正常发送并在客户端收听, 音质随文件真实格式而决定. + * + * 因此在发送时 [codec] 通常可以总是使用 [AudioCodec.SILK] (这也是 [AudioSupported.uploadAudio] 的默认行为). + */ + public val codec: AudioCodec + + /** + * 文件的额外数据. 该数据由服务器提供, 可能会影响语音音质等属性. + * [extraData] 为 `null` 时也可以发送语音, 但不确定发送和客户端收听是否会正常. + * + * [extraData] 可能随服务器更新而更新, 因此请不要尝试解析该数据. + * + * [extraData] 向下兼容, 即旧版本的 [extraData] 可以在新版本构造 [OfflineAudio] ([OfflineAudio.Factory.create]). + */ + public val extraData: ByteArray? + + /** + * @return `"[mirai:audio:${filename}]"` + */ + public override fun toString(): String + public override fun contentToString(): String = "[语音消息]" +} + + +/** + * 在线语音消息, 即从消息事件中接收到的语音消息. + * + * [OnlineAudio] 可以获取[语音长度][length]以及[下载链接][urlForDownload]. + * + * [OnlineAudio] 仅可以从事件中的[消息链][MessageChain]接收, 不可手动构造. 若需要手动构造, 请使用 [OfflineAudio.Factory.create] 构造 [离线语音][OfflineAudio]. + * + * ### 序列化支持 + * + * [OnlineAudio] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString]. + * 也可以在 [MessageSerializers.serializersModule] 获取到 [OnlineAudio] 的 [KSerializer]. + * + * 要获取更多有关序列化的信息, 参阅 [MessageSerializers]. + * + * ### 不建议自行实现该接口 + * + * [OnlineAudio] 不稳定, 将来可能会增加新的抽象属性或方法而导致不兼容. 仅可以使用该接口而不能继承或实现它. + * + * @since 2.7 + * @see OfflineAudio + */ +@NotStableForInheritance +public interface OnlineAudio : Audio { // 协议实现 + /** + * 下载链接 HTTP URL. + * @return `"http://xxx"` + */ + public val urlForDownload: String + + /** + * 语音长度秒数 + */ + public val length: Long + + public companion object Key : + AbstractPolymorphicMessageKey<Audio, OnlineAudio>(Audio, { it.safeCast() }) { + + public const val SERIAL_NAME: String = "OnlineAudio" + } +} + +/** + * 离线语音消息. + * + * [OfflineAudio] 仅拥有协议上必要的五个属性: + * - 文件名 [filename] + * - 文件 MD5 [fileMd5] + * - 文件大小 [fileSize] + * - 编码方式 [codec] + * - 额外数据 [extraData] + * + * [OfflineAudio] 可由本地 [ExternalResource] 经过 [AudioSupported.uploadAudio] 上传到服务器得到, 故无[下载链接][OnlineAudio.urlForDownload]. + * + * [OfflineAudio] 同时还可以用做自定义构造 [Audio] 实例, 使用 [OfflineAudio.Factory.create] 可通过上述五个必要参数获得 [OfflineAudio] 实例. + * + * ### 序列化支持 + * + * [OfflineAudio] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString]. + * 也可以在 [MessageSerializers.serializersModule] 获取到 [OfflineAudio] 的 [KSerializer]. + * + * 要获取更多有关序列化的信息, 参阅 [MessageSerializers]. + * + * ### 不建议自行实现该接口 + * + * [OfflineAudio] 不稳定, 将来可能会增加新的抽象属性或方法而导致不兼容. 仅可以使用该接口而不能继承或实现它. + * + * @since 2.7 + */ +@NotStableForInheritance +public interface OfflineAudio : Audio { + + public companion object Key : + AbstractPolymorphicMessageKey<Audio, OfflineAudio>(Audio, { it.safeCast() }) { + public const val SERIAL_NAME: String = "OfflineAudio" + } + + public interface Factory { + /** + * 构造 [OfflineAudio]. 有关参数的含义, 参考 [Audio]. + * + * 在 Kotlin 可以使用类构造器的函数 [OfflineAudio]: `OfflineAudio(...)` + */ + public fun create( + filename: String, + fileMd5: ByteArray, + fileSize: Long, + codec: AudioCodec, + extraData: ByteArray?, + ): OfflineAudio + + /** + * 使用 [OnlineAudio] 的信息构造 [OfflineAudio]. + * + * 在 Kotlin 可以使用类构造器的函数 [OfflineAudio]: `OfflineAudio(...)` + */ + public fun from(onlineAudio: OnlineAudio): OfflineAudio = onlineAudio.run { + create(filename, fileMd5, fileSize, codec, extraData) + } + + public companion object INSTANCE : + Factory by loadService("net.mamoe.mirai.internal.message.OfflineAudioFactoryImpl") + } +} + +/** + * 构造 [OfflineAudio]. 有关参数的含义, 参考 [Audio]. + * @since 2.7 + */ +@JvmSynthetic +public inline fun OfflineAudio( + filename: String, + fileMd5: ByteArray, + fileSize: Long, + codec: AudioCodec, + extraData: ByteArray?, +): OfflineAudio = OfflineAudio.Factory.create(filename, fileMd5, fileSize, codec, extraData) + +/** + * 使用 [OnlineAudio] 的信息构造 [OfflineAudio]. + * @since 2.7 + */ +@JvmSynthetic +public inline fun OfflineAudio( + onlineAudio: OnlineAudio +): OfflineAudio = OfflineAudio.Factory.from(onlineAudio) + + +/** + * 语音编码方式. + * + * @since 2.7 + */ +@Serializable(AudioCodec.AsIntSerializer::class) +public enum class AudioCodec( + public val id: Int, + public val formatName: String, +) { + /** + * 低音质编码格式 + */ + AMR(0, "amr"), + + /** + * 高音质编码格式 + */ + SILK(1, "silk"); + + public companion object { + private val VALUES = values() + + @JvmStatic + public fun fromId(id: Int): AudioCodec = VALUES.first { it.id == id } + + @JvmStatic + public fun fromFormatName(formatName: String): AudioCodec = VALUES.first { it.formatName == formatName } + + @JvmStatic + public fun fromIdOrNull(id: Int): AudioCodec? = VALUES.find { it.id == id } + + @JvmStatic + public fun fromFormatNameOrNull(formatName: String): AudioCodec? = + VALUES.find { it.formatName == formatName } + } + + internal object AsIntSerializer : KSerializer<AudioCodec> by Int.serializer().map( + Int.serializer().descriptor, + deserialize = { fromId(it) }, + serialize = { id } + ) +} \ No newline at end of file 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 063fdc561..34e26784b 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt @@ -7,16 +7,20 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:JvmMultifileClass +@file:JvmName("MessageUtils") +@file:Suppress("NOTHING_TO_INLINE") + 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.* -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.NotStableForInheritance +import net.mamoe.mirai.utils.safeCast /** @@ -49,56 +53,68 @@ public abstract class PttMessage : MessageContent { @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 } /** - * 语音消息, 目前只支持接收和转发 + * 已弃用的旧版本语音消息. * - * 目前, 使用 [Voice] 类型是稳定的, 但调用 [Voice] 中的属性 [fileName], [md5], [fileSize] 是不稳定的. 语音的序列化也可能会在未来有变动. + * [Voice] 由于有设计缺陷已弃用且可能会在将来版本删除, 请使用 [Audio]. * - * ## 使用语音 + * ## 迁移指南 * - * 可以通过 [ExternalResource.uploadAsVoice] 或者 [Group.uploadVoice] 上传语音文件到服务器, 得到 [Voice] 实例. 但这不会发送给目标群. - * 上传后需要通过 [Group.sendMessage] 发送 [Voice] 实例. - * - * [Voice] 实例可以通过序列化方式保存. 下次可以用它发送因而不需要上传. 但可能由于未来服务器更新, 这项功能就不稳定. 因此建议总是上传音频文件而不要保存 [Voice]. + * - 将使用的 [Voice] 类型替换为 [Audio] 类型 + * - 将 [Group.uploadVoice] 替换为 [Group.uploadAudio] + * - 如果有必须使用旧 [Voice] 类型的情况, 请使用 [Audio.toVoice] */ -@Suppress("DuplicatedCode") -@Serializable(Voice.Serializer::class) // experimental +@Suppress("DuplicatedCode", "DEPRECATION") +@Serializable @SerialName(Voice.SERIAL_NAME) -public class Voice @MiraiInternalApi constructor( +@Deprecated( + "Please use Audio instead.", + replaceWith = ReplaceWith("Audio", "net.mamoe.mirai.message.data.Audio"), + level = DeprecationLevel.WARNING +) +public open class Voice @MiraiInternalApi constructor( @MiraiExperimentalApi public override val fileName: String, @MiraiExperimentalApi public override val md5: ByteArray, @MiraiExperimentalApi public override val fileSize: Long, - @MiraiInternalApi public val codec: Int = 0, + @SerialName("codec") @MiraiInternalApi public val _codec: Int = 0, private val _url: String ) : PttMessage() { public companion object Key : AbstractPolymorphicMessageKey<PttMessage, Voice>(PttMessage, { it.safeCast() }) { public const val SERIAL_NAME: String = "Voice" + + /** + * 将 2.7 新增的 [Audio] 转为旧版本的 [Voice], 以兼容某些情况. + * + * @see Audio.toVoice + * @since 2.7 + */ + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated( + "Please consider migrating to Audio", + level = DeprecationLevel.WARNING + ) + @JvmStatic + public fun fromAudio(audio: Audio): Voice { + audio.run { + return Voice( + filename, + fileMd5, + fileSize, + codec.id, + if (this is OnlineAudio) kotlin.runCatching { urlForDownload }.getOrElse { "" } else "" + ) + } + } } /** * 下载链接 HTTP URL. */ - public val url: String? + public open val url: String? get() = when { _url.isBlank() -> null _url.startsWith("http") -> _url @@ -115,69 +131,54 @@ public class Voice @MiraiInternalApi constructor( public override fun contentToString(): String = "[语音消息]" + /** + * 转换为 2.7 新增的 [Audio], 以兼容某些无法迁移的情况. + * + * @since 2.7 + */ + public fun toAudio(): Audio { + val voice = this + return OfflineAudio( + voice.fileName, + voice.md5, + voice.fileSize, + AudioCodec.fromIdOrNull(voice._codec) ?: AudioCodec.SILK, + byteArrayOf() + ) + } + 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 (_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 = 33 * result + _codec result = 15 * result + _url.hashCode() return result } +} - public object Serializer : KSerializer<Voice> 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 +/** + * 将 2.7 新增的 [Audio] 转为旧版本的 [Voice], 以兼容某些情况. + * + * @since 2.7 + */ +@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith") +@Deprecated( + "Please migrate to Audio", + level = DeprecationLevel.WARNING +) +@JvmSynthetic +public inline fun Audio.toVoice(): Voice = Voice.fromAudio(this) \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index 3f3636bea..56f8ac3cd 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -19,13 +19,12 @@ import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.contact.Contact.Companion.uploadImage import net.mamoe.mirai.contact.FileSupported -import net.mamoe.mirai.contact.VoiceSupported +import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.Voice import net.mamoe.mirai.message.data.sendTo import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource @@ -416,58 +415,15 @@ public interface ExternalResource : Closeable { // region uploadAsVoice /////////////////////////////////////////////////////////////////////////// - /** - * 将文件作为语音上传后构造 [Voice]. 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友. - * - * **服务器仅支持音频格式 `silk` 或 `amr`**. 需要调用方手动[关闭资源][ExternalResource.close]. - * - * @throws OverFileSizeMaxException - * @since 2.7 - * @see VoiceSupported.uploadVoice - */ + @Suppress("DEPRECATION") @JvmBlockingBridge @JvmStatic - public suspend fun ExternalResource.uploadAsVoice(contact: VoiceSupported): Voice { - return contact.uploadVoice(this) - } - - @JvmBlockingBridge - @JvmStatic - @Deprecated("For binary compatibility", level = DeprecationLevel.WARNING) - @JvmName("uploadAsVoice") - public suspend fun ExternalResource.uploadAsVoice(contact: Contact): Voice { - if (contact is VoiceSupported) return contact.uploadVoice(this) + @Deprecated("Use `contact.uploadAudio(resource)` instead", level = DeprecationLevel.WARNING) + public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice { + @Suppress("DEPRECATION") + if (contact is Group) return contact.uploadVoice(this) else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice") } - - /** - * 读取 [InputStream] 到临时文件并将其作为语音上传至 [contact] 后构造 [Voice], - * 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友 - * - * 注意:本函数不会关闭流. - * - * @since 2.7 - * @throws OverFileSizeMaxException - * @see VoiceSupported.uploadVoice - */ - @JvmStatic - @JvmBlockingBridge - public suspend fun InputStream.uploadAsVoice(contact: VoiceSupported): Voice = - runBIO { toExternalResource() }.withUse { uploadAsVoice(contact) } - - /** - * 将文件作为语音上传后构造 [Voice]. - * 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友 - * - * @since 2.7 - * @throws OverFileSizeMaxException - * @see VoiceSupported.uploadVoice - */ - @JvmStatic - @JvmBlockingBridge - public suspend fun File.uploadAsVoice(contact: VoiceSupported): Voice = - toExternalResource().withUse { uploadAsVoice(contact) } - // endregion } } diff --git a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt index 5f4da0bf2..733beb89f 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt @@ -149,7 +149,7 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len } public inline fun ByteArray.encodeToString(offset: Int = 0, charset: Charset = Charsets.UTF_8): String = - kotlinx.io.core.String(this, charset = charset, offset = offset, length = this.size - offset) + String(this, charset = charset, offset = offset, length = this.size - offset) public expect fun ByteArray.encodeBase64(): String public expect fun String.decodeBase64(): ByteArray diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 63e7cd27f..472201b3c 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -124,6 +124,14 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { UnsupportedMessageImpl::class, UnsupportedMessageImpl.serializer() ) + MessageSerializers.registerSerializer( + OnlineAudioImpl::class, + OnlineAudioImpl.serializer() + ) + MessageSerializers.registerSerializer( + OfflineAudioImpl::class, + OfflineAudioImpl.serializer() + ) } } @@ -965,14 +973,4 @@ 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/commonMain/kotlin/contact/FriendImpl.kt b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt index a33fc9294..d0d80a0ba 100644 --- a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt @@ -9,11 +9,7 @@ @file:OptIn(LowLevelApi::class) @file:Suppress( - "EXPERIMENTAL_API_USAGE", - "DEPRECATION_ERROR", "NOTHING_TO_INLINE", - "INVISIBLE_MEMBER", - "INVISIBLE_REFERENCE" ) package net.mamoe.mirai.internal.contact @@ -28,18 +24,19 @@ import net.mamoe.mirai.event.events.FriendMessagePostSendEvent import net.mamoe.mirai.event.events.FriendMessagePreSendEvent import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.message.OfflineAudioImpl import net.mamoe.mirai.internal.network.highway.* import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore -import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec +import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.Voice +import net.mamoe.mirai.message.data.OfflineAudio import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.recoverCatchingSuppressed import net.mamoe.mirai.utils.toByteArray @@ -78,10 +75,9 @@ internal class FriendImpl( "Friend ${this.id} had already been deleted" } bot.network.run { - FriendList.DelFriend.invoke(bot.client, this@FriendImpl) - .sendAndExpect<FriendList.DelFriend.Response>().also { - check(it.isSuccess) { "delete friend failed: ${it.resultCode}" } - } + FriendList.DelFriend.invoke(bot.client, this@FriendImpl).sendAndExpect().also { + check(it.isSuccess) { "delete friend failed: ${it.resultCode}" } + } } } @@ -95,19 +91,13 @@ internal class FriendImpl( override fun toString(): String = "Friend($id)" - override suspend fun uploadVoice(resource: ExternalResource): Voice = bot.network.run { - val voice = Voice( - "${resource.md5.toUHexString("")}.amr", - resource.md5, - resource.size, - resource.voiceCodec, - "" - ) + override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = bot.network.run { + var audio: OfflineAudioImpl? = null kotlin.runCatching { val resp = Highway.uploadResourceBdh( bot = bot, resource = resource, - kind = ResourceKind.PRIVATE_VOICE, + kind = ResourceKind.PRIVATE_AUDIO, commandId = 26, extendInfo = PttStore.C2C.createC2CPttStoreBDHExt(bot, this@FriendImpl.uin, resource) .toByteArray(Cmd0x346.ReqBody.serializer()) @@ -117,14 +107,20 @@ internal class FriendImpl( if (c346resp.msgApplyUploadRsp == null) { error("Upload failed") } - voice.pttInternalInstance = ImMsgBody.Ptt( - fileType = 4, - srcUin = bot.uin, - fileUuid = c346resp.msgApplyUploadRsp.uuid, + audio = OfflineAudioImpl( + filename = "${resource.md5.toUHexString("")}.amr", fileMd5 = resource.md5, - fileName = resource.md5 + ".amr".toByteArray(), - fileSize = resource.size.toInt(), - boolValid = true, + fileSize = resource.size, + codec = resource.audioCodec, + originalPtt = ImMsgBody.Ptt( + fileType = 4, + srcUin = bot.uin, + fileUuid = c346resp.msgApplyUploadRsp.uuid, + fileMd5 = resource.md5, + fileName = resource.md5 + ".amr".toByteArray(), + fileSize = resource.size.toInt(), + boolValid = true, + ) ) }.recoverCatchingSuppressed { when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect<Any>()) { @@ -133,24 +129,30 @@ internal class FriendImpl( bot, resp.uploadIpList.zip(resp.uploadPortList), resource.size, - ResourceKind.GROUP_VOICE, + ResourceKind.GROUP_AUDIO, ChannelKind.HTTP ) { ip, port -> Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey) } - voice.pttInternalInstance = ImMsgBody.Ptt( - fileType = 4, - srcUin = bot.uin, - fileUuid = resp.fileId.toByteArray(), + audio = OfflineAudioImpl( + filename = "${resource.md5.toUHexString("")}.amr", fileMd5 = resource.md5, - fileName = resource.md5 + ".amr".toByteArray(), - fileSize = resource.size.toInt(), - boolValid = true, + fileSize = resource.size, + codec = resource.audioCodec, + originalPtt = ImMsgBody.Ptt( + fileType = 4, + srcUin = bot.uin, + fileUuid = resp.fileId.toByteArray(), + fileMd5 = resource.md5, + fileName = resource.md5 + ".amr".toByteArray(), + fileSize = resource.size.toInt(), + boolValid = true, + ) ) } } }.getOrThrow() - return voice + return audio!! } } diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 929bf63f5..63f773f9c 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -23,22 +23,25 @@ import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.message.OfflineAudioImpl import net.mamoe.mirai.internal.message.OfflineGroupImage import net.mamoe.mirai.internal.network.components.BdhSession import net.mamoe.mirai.internal.network.handler.NetworkHandler import net.mamoe.mirai.internal.network.handler.logger import net.mamoe.mirai.internal.network.highway.ChannelKind import net.mamoe.mirai.internal.network.highway.Highway +import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_AUDIO import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_IMAGE -import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_VOICE import net.mamoe.mirai.internal.network.highway.postPtt import net.mamoe.mirai.internal.network.highway.tryServersUpload import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388 import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopEssenceMsgManager import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore +import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache import net.mamoe.mirai.internal.utils.RemoteFileImpl import net.mamoe.mirai.internal.utils.io.serialization.toByteArray @@ -204,32 +207,10 @@ internal class GroupImpl( } } + @Suppress("OverridingDeprecatedMember", "DEPRECATION") override suspend fun uploadVoice(resource: ExternalResource): Voice { return bot.network.run { - kotlin.runCatching { - val (_) = Highway.uploadResourceBdh( - bot = bot, - resource = resource, - kind = GROUP_VOICE, - commandId = 29, - extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource) - .toByteArray(Cmd0x388.ReqBody.serializer()), - ) - }.recoverCatchingSuppressed { - when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect<Any>()) { - is PttStore.GroupPttUp.Response.RequireUpload -> { - tryServersUpload( - bot, - resp.uploadIpList.zip(resp.uploadPortList), - resource.size, - GROUP_VOICE, - ChannelKind.HTTP - ) { ip, port -> - Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey) - } - } - } - }.getOrThrow() + uploadAudioResource(resource) // val body = resp?.loadAs(Cmd0x388.RspBody.serializer()) // ?.msgTryupPttRsp @@ -243,6 +224,51 @@ internal class GroupImpl( "" ) } + } + + private suspend fun uploadAudioResource(resource: ExternalResource) { + kotlin.runCatching { + val (_) = Highway.uploadResourceBdh( + bot = bot, + resource = resource, + kind = GROUP_AUDIO, + commandId = 29, + extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource) + .toByteArray(Cmd0x388.ReqBody.serializer()), + ) + }.recoverCatchingSuppressed { + when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect(bot)) { + is PttStore.GroupPttUp.Response.RequireUpload -> { + tryServersUpload( + bot, + resp.uploadIpList.zip(resp.uploadPortList), + resource.size, + GROUP_AUDIO, + ChannelKind.HTTP + ) { ip, port -> + Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey) + } + } + } + }.getOrThrow() + } + + override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio { + return bot.network.run { + uploadAudioResource(resource) + + // val body = resp?.loadAs(Cmd0x388.RspBody.serializer()) + // ?.msgTryupPttRsp + // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey") + + OfflineAudioImpl( + filename = "${resource.md5.toUHexString("")}.amr", + fileMd5 = resource.md5, + fileSize = resource.size, + codec = resource.audioCodec, + originalPtt = null, + ) + } } diff --git a/mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt b/mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt new file mode 100644 index 000000000..9ad621c3f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt @@ -0,0 +1,302 @@ +/* + * Copyright 2019-2021 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.io.core.toByteArray +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.utils.io.ProtoBuf +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.copy +import net.mamoe.mirai.utils.map + + +/** + * ## Audio Implementation Overview + * + * ``` + * (api)Audio + * | + * /------------------\ + * (api)OnlineAudio (api)OfflineAudio + * | | + * | | + * (core)OnlineAudioImpl (core)OfflineAudioImpl + * ``` + * + * - [OnlineAudioImpl]: 实现从 [ImMsgBody.Ptt] 解析 + * - [OfflineAudioImpl]: 支持用户手动构造 + * + * ## Equality + * + * - [OnlineAudio] != [OfflineAudio] + * + * ## Converting [Audio] to [ImMsgBody.Ptt] + * + * Always call [Audio.toPtt] + */ +internal interface AudioPttSupport : MessageContent { // Audio is sealed in mirai-core-api + /** + * 原协议数据. 用于在接受到其他用户发送的语音时能按照原样发回. + */ + val originalPtt: ImMsgBody.Ptt? +} + +@Serializable +internal class AudioExtraData( + @ProtoNumber(1) val ptt: ImMsgBody.Ptt?, +) : ProtoBuf { + fun toByteArray(): ByteArray { + return Wrapper(CURRENT_VERSION, this).toByteArray(Wrapper.serializer()) + } + + companion object { + @Serializable + class Wrapper( + @ProtoNumber(1) val version: Int, + @ProtoNumber(2) val v1: AudioExtraData, + ) : ProtoBuf + + private const val CURRENT_VERSION = 1 + + + fun loadFrom(byteArray: ByteArray?): AudioExtraData? { + byteArray ?: return null + return kotlin.runCatching { + byteArray.loadAs(Wrapper.serializer()).v1 + }.getOrNull() + } + } +} + +internal fun Audio.toPtt(): ImMsgBody.Ptt { + if (this is AudioPttSupport) { + this.originalPtt?.let { return it } + } + return ImMsgBody.Ptt( + fileName = this.filename.toByteArray(), + fileMd5 = this.fileMd5, + boolValid = true, + fileSize = this.fileSize.toInt(), + fileType = 4, + pbReserve = byteArrayOf(0), + format = this.codec.id + ) +} + +@SerialName(OnlineAudio.SERIAL_NAME) +@Serializable(OnlineAudioImpl.Serializer::class) +internal class OnlineAudioImpl( + override val filename: String, + override val fileMd5: ByteArray, + override val fileSize: Long, + override val codec: AudioCodec, + url: String, + override val length: Long, + override val originalPtt: ImMsgBody.Ptt?, +) : OnlineAudio, AudioPttSupport { + private val _url = refineUrl(url) + + override val extraData: ByteArray? by lazy { + AudioExtraData(originalPtt).toByteArray() + } + + override val urlForDownload: String + get() = _url.takeIf { it.isNotBlank() } + ?: throw UnsupportedOperationException("Could not fetch URL for audio $filename") + + private val _stringValue: String by lazy { "[mirai:audio:${filename}]" } + override fun toString(): String = _stringValue + + @Suppress("DuplicatedCode") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OnlineAudioImpl + + if (filename != other.filename) return false + if (!fileMd5.contentEquals(other.fileMd5)) return false + if (fileSize != other.fileSize) return false + if (_url != other._url) return false + if (codec != other.codec) return false + if (length != other.length) return false + if (originalPtt != other.originalPtt) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + filename.hashCode() + result = 31 * result + fileMd5.contentHashCode() + result = 31 * result + fileSize.hashCode() + result = 31 * result + _url.hashCode() + result = 31 * result + codec.hashCode() + result = 31 * result + length.hashCode() + result = 31 * result + originalPtt.hashCode() + return result + } + + + companion object { + fun refineUrl(url: String) = when { + url.isBlank() -> "" + url.startsWith("http") -> url + url.startsWith("/") -> "$DOWNLOAD_URL$url" + else -> "$DOWNLOAD_URL/$url" + } + + @Suppress("HttpUrlsUsage") + const val DOWNLOAD_URL = "http://grouptalk.c2c.qq.com" + } + + object Serializer : KSerializer<OnlineAudioImpl> by Surrogate.serializer().map( + resultantDescriptor = Surrogate.serializer().descriptor.copy(OnlineAudio.SERIAL_NAME), + deserialize = { + OnlineAudioImpl( + filename = filename, + fileMd5 = fileMd5, + fileSize = fileSize, + url = urlForDownload, + codec = codec, + length = length, + originalPtt = AudioExtraData.loadFrom(extraData)?.ptt + ) + }, + serialize = { + Surrogate( + filename = filename, + fileMd5 = fileMd5, + fileSize = fileSize, + urlForDownload = urlForDownload, + codec = codec, + length = length, + extraData = extraData + ) + } + ) { + @Serializable + @SerialName(OnlineAudio.SERIAL_NAME) + private class Surrogate( + override val filename: String, + override val fileMd5: ByteArray, + override val fileSize: Long, + override val codec: AudioCodec, + override val length: Long, + override val extraData: ByteArray?, + override val urlForDownload: String, + ) : OnlineAudio { + override fun toString(): String { + return "Surrogate(filename='$filename', fileMd5=${fileMd5.contentToString()}, fileSize=$fileSize, codec=$codec, length=$length, extraData=${extraData.contentToString()}, urlForDownload='$urlForDownload')" + } + } + } +} + +@SerialName(OfflineAudio.SERIAL_NAME) +@Serializable(OfflineAudioImpl.Serializer::class) +internal class OfflineAudioImpl( + override val filename: String, + override val fileMd5: ByteArray, + override val fileSize: Long, + override val codec: AudioCodec, + override val originalPtt: ImMsgBody.Ptt?, +) : OfflineAudio, AudioPttSupport { + constructor( + filename: String, + fileMd5: ByteArray, + fileSize: Long, + codec: AudioCodec, + extraData: ByteArray?, + ) : this(filename, fileMd5, fileSize, codec, AudioExtraData.loadFrom(extraData)?.ptt) + + override val extraData: ByteArray? by lazy { + AudioExtraData(originalPtt).toByteArray() + } + + private val _stringValue: String by lazy { "[mirai:audio:${filename}]" } + override fun toString(): String = _stringValue + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OfflineAudioImpl + + if (filename != other.filename) return false + if (!fileMd5.contentEquals(other.fileMd5)) return false + if (fileSize != other.fileSize) return false + if (codec != other.codec) return false + if (originalPtt != other.originalPtt) return false + return true + } + + override fun hashCode(): Int { + var result = filename.hashCode() + result = 31 * result + fileMd5.contentHashCode() + result = 31 * result + fileSize.hashCode() + result = 31 * result + codec.hashCode() + result = 31 * result + originalPtt.hashCode() + return result + } + + object Serializer : KSerializer<OfflineAudioImpl> by Surrogate.serializer().map( + resultantDescriptor = Surrogate.serializer().descriptor.copy(OfflineAudio.SERIAL_NAME), + deserialize = { + OfflineAudioImpl( + filename = filename, + fileMd5 = fileMd5, + fileSize = fileSize, + codec = codec, + extraData = extraData, + ) + }, + serialize = { + Surrogate( + filename = filename, + fileMd5 = fileMd5, + fileSize = fileSize, + codec = codec, + extraData = extraData, + ) + } + ) { + @Serializable + @SerialName(OfflineAudio.SERIAL_NAME) + private class Surrogate( + override val filename: String, + override val fileMd5: ByteArray, + override val fileSize: Long, + override val codec: AudioCodec, + override val extraData: ByteArray?, + ) : OfflineAudio { + override fun toString(): String { + return "OfflineAudio(filename='$filename', fileMd5=${fileMd5.contentToString()}, fileSize=$fileSize, codec=$codec, extraData=${extraData.contentToString()})" + } + } + } +} + +@PublishedApi +internal class OfflineAudioFactoryImpl : OfflineAudio.Factory { + override fun create( + filename: String, + fileMd5: ByteArray, + fileSize: Long, + codec: AudioCodec, + extraData: ByteArray? + ): OfflineAudio = OfflineAudioImpl(filename, fileMd5, fileSize, codec, extraData) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index 088f485a9..a7f1c473f 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -18,15 +18,12 @@ import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain -import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toVoice +import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio import net.mamoe.mirai.internal.network.protocol.data.proto.* import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.encodeToString -import net.mamoe.mirai.utils.read -import net.mamoe.mirai.utils.toUHexString -import net.mamoe.mirai.utils.unzip +import net.mamoe.mirai.utils.* /** * 只在手动构造 [OfflineMessageSource] 时调用 @@ -91,7 +88,7 @@ private fun List<MsgComm.Msg>.toMessageChain( joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder) for (msg in messageList) { - msg.msgBody.richText.ptt?.toVoice()?.let { builder.add(it) } + msg.msgBody.richText.ptt?.toAudio()?.let { builder.add(it) } } return builder.build().cleanupRubbishMessageElements() @@ -516,11 +513,13 @@ internal object ReceiveMessageTransformer { } } - fun ImMsgBody.Ptt.toVoice() = Voice( - kotlinx.io.core.String(fileName), - fileMd5, - fileSize.toLong(), - format, - kotlinx.io.core.String(downPara) - ).also { it.pttInternalInstance = this } + fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl( + filename = fileName.encodeToString(), + fileMd5 = fileMd5, + fileSize = fileSize.toLongUnsigned(), + codec = AudioCodec.fromId(format), + url = downPara.encodeToString(), + length = time.toLongUnsigned(), + originalPtt = this, + ) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index aa9014a8b..4b0f83567 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -122,8 +122,8 @@ internal enum class ResourceKind( ) { PRIVATE_IMAGE("private image"), GROUP_IMAGE("group image"), - PRIVATE_VOICE("private voice"), - GROUP_VOICE("group voice"), + PRIVATE_AUDIO("private audio"), + GROUP_AUDIO("group audio"), GROUP_FILE("group file"), diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt index 87f8c9034..3cbd88616 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt @@ -765,7 +765,69 @@ internal class ImMsgBody : ProtoBuf { @ProtoNumber(30) @JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY, @ProtoNumber(31) @JvmField val bytesPttUrls: List<ByteArray> = emptyList(), @ProtoNumber(32) @JvmField val downloadFlag: Int = 0, - ) : ProtoBuf + ) : ProtoBuf { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Ptt + + if (fileType != other.fileType) return false + if (srcUin != other.srcUin) return false + if (!fileUuid.contentEquals(other.fileUuid)) return false + if (!fileMd5.contentEquals(other.fileMd5)) return false + if (!fileName.contentEquals(other.fileName)) return false + if (fileSize != other.fileSize) return false + if (!reserve.contentEquals(other.reserve)) return false + if (fileId != other.fileId) return false + if (serverIp != other.serverIp) return false + if (serverPort != other.serverPort) return false + if (boolValid != other.boolValid) return false + if (!signature.contentEquals(other.signature)) return false + if (!shortcut.contentEquals(other.shortcut)) return false + if (!fileKey.contentEquals(other.fileKey)) return false + if (magicPttIndex != other.magicPttIndex) return false + if (voiceSwitch != other.voiceSwitch) return false + if (!pttUrl.contentEquals(other.pttUrl)) return false + if (!groupFileKey.contentEquals(other.groupFileKey)) return false + if (time != other.time) return false + if (!downPara.contentEquals(other.downPara)) return false + if (format != other.format) return false + if (!pbReserve.contentEquals(other.pbReserve)) return false + if (bytesPttUrls != other.bytesPttUrls) return false + if (downloadFlag != other.downloadFlag) return false + + return true + } + + override fun hashCode(): Int { + var result = fileType + result = 31 * result + srcUin.hashCode() + result = 31 * result + fileUuid.contentHashCode() + result = 31 * result + fileMd5.contentHashCode() + result = 31 * result + fileName.contentHashCode() + result = 31 * result + fileSize + result = 31 * result + reserve.contentHashCode() + result = 31 * result + fileId + result = 31 * result + serverIp + result = 31 * result + serverPort + result = 31 * result + boolValid.hashCode() + result = 31 * result + signature.contentHashCode() + result = 31 * result + shortcut.contentHashCode() + result = 31 * result + fileKey.contentHashCode() + result = 31 * result + magicPttIndex + result = 31 * result + voiceSwitch + result = 31 * result + pttUrl.contentHashCode() + result = 31 * result + groupFileKey.contentHashCode() + result = 31 * result + time + result = 31 * result + downPara.contentHashCode() + result = 31 * result + format + result = 31 * result + pbReserve.contentHashCode() + result = 31 * result + bytesPttUrls.hashCode() + result = 31 * result + downloadFlag + return result + } + } @Serializable internal class PubAccInfo( diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt index 0f80ec9b4..fc5f801a8 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt @@ -145,7 +145,8 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. return response } - internal fun PttMessage.toPtt() = run { + // old Voice + private fun PttMessage.toPtt() = run { (this.pttInternalInstance as? ImMsgBody.Ptt)?.let { return it } ImMsgBody.Ptt( fileName = fileName.toByteArray(), @@ -155,8 +156,9 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. fileType = 4, pbReserve = byteArrayOf(0), format = let { + @Suppress("DEPRECATION") if (it is Voice) { - it.codec + it._codec } else { 0 } @@ -244,7 +246,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. ImMsgBody.MsgBody( richText = ImMsgBody.RichText( elems = subChain.toRichTextElems(messageTarget = targetFriend, withGeneralFlags = true), - ptt = subChain[PttMessage]?.toPtt(), + ptt = subChain.findPtt(), ) ) }, @@ -361,7 +363,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. ImMsgBody.MsgBody( richText = ImMsgBody.RichText( elems = subChain.toRichTextElems(messageTarget = targetGroup, withGeneralFlags = true), - ptt = subChain[PttMessage]?.toPtt() + ptt = subChain.findPtt() ) ) @@ -409,6 +411,9 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. doFragmented = fragmented ) } + + private fun MessageChain.findPtt() = + findIsInstance<Audio>()?.toPtt() ?: this[PttMessage]?.toPtt() /* = buildOutgoingUniPacket(client) { ///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes()) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt index 0c36fd20e..1b7a4c61f 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt @@ -22,18 +22,21 @@ import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.internal.utils.toIpV4AddressString +import net.mamoe.mirai.message.data.AudioCodec import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.encodeToString import net.mamoe.mirai.utils.toUHexString -internal val ExternalResource.voiceCodec: Int +internal inline val ExternalResource.voiceCodec: Int get() = audioCodec.id + +internal val ExternalResource.audioCodec: AudioCodec get() { return when (formatName) { // 实际上 amr 是 0, 但用 1 也可以发. 为了避免 silk 错被以 amr 发送导致降音质就都用 1 - "amr" -> 1 // amr - "silk" -> 1 // silk V3 - else -> 1 // use amr by default + "amr" -> AudioCodec.SILK + "silk" -> AudioCodec.SILK + else -> AudioCodec.AMR // use amr by default } } diff --git a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory new file mode 100644 index 000000000..d995a6641 --- /dev/null +++ b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory @@ -0,0 +1,10 @@ +# +# Copyright 2019-2021 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 +# + +net.mamoe.mirai.internal.message.OfflineAudioFactoryImpl \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/AudioTest.kt b/mirai-core/src/commonTest/kotlin/message/AudioTest.kt new file mode 100644 index 000000000..f02720f3d --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/AudioTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019-2021 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 net.mamoe.mirai.internal.message.OnlineAudioImpl.Companion.DOWNLOAD_URL +import net.mamoe.mirai.internal.message.OnlineAudioImpl.Companion.refineUrl +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.message.data.AudioCodec +import net.mamoe.mirai.message.data.OfflineAudio +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +internal class AudioTest : AbstractTest() { + + @Test + fun `test factory`() { + assertEquals( + OfflineAudio("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()), + OfflineAudio("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()) + ) + } + + @Test + fun `invalid extraData is refreshed`() { + assertContentEquals( + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null).extraData, + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2, 3)).extraData, + ) + } + + @Test + fun `test equality`() { + assertEquals( + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null), + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null) + ) + assertEquals( + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null), + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null) + ) + assertEquals( + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, originalPtt = null), + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, originalPtt = null) + ) + assertEquals( + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()), + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()) + ) + assertEquals( + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, ImMsgBody.Ptt(srcUin = 2)), + OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, ImMsgBody.Ptt(srcUin = 2)) + ) + } + + @Test + fun `test refineUrl`() { + assertFalse { DOWNLOAD_URL.endsWith("/") } + + assertEquals("", refineUrl("")) + assertEquals("$DOWNLOAD_URL/test", refineUrl("/test")) + assertEquals("$DOWNLOAD_URL/test", refineUrl("test")) + assertEquals("https://custom.com", refineUrl("https://custom.com")) + assertEquals("http://localhost", refineUrl("http://localhost")) + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt b/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt index 10dbc2aa7..abcf30fe6 100644 --- a/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt +++ b/mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt @@ -12,13 +12,12 @@ package net.mamoe.mirai.internal.message.data import kotlinx.serialization.KSerializer import kotlinx.serialization.Polymorphic import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.* import kotlinx.serialization.serializer import net.mamoe.mirai.Mirai import net.mamoe.mirai.internal.message.FileMessageImpl import net.mamoe.mirai.internal.message.MarketFaceImpl +import net.mamoe.mirai.internal.message.OnlineAudioImpl import net.mamoe.mirai.internal.message.UnsupportedMessageImpl import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.utils._miraiContentToString @@ -220,39 +219,100 @@ internal class MessageSerializationTest { @Serializable data class V( - val msg: Voice + val msg: Audio + ) + + @Serializable + data class AudioTestStandard( + val online: OnlineAudio, + val offline: OfflineAudio, + val onlineAsRec: Audio, + val offlineAsRec: Audio, ) @Test - fun `test Voice serialization`() { - val v = V(Voice("4517", byteArrayOf(14), 50, 3, "https://github.com")) - println(v.serialize(V.serializer())) + fun `test Audio standard`() { + val origin = AudioTestStandard( + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null), + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null), + + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null), + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null), + ) + assertEquals( - v.serialize(V.serializer()), - v.serialize(V.serializer()) - .deserialize(V.serializer()) - .serialize(V.serializer()) + AudioCodec.SILK.id, + format.encodeToJsonElement(origin).jsonObject["offline"]!!.jsonObject["codec"]!!.jsonPrimitive.content.toInt() + ) // use custom serializer + + assertEquals( + AudioCodec.SILK.id, + format.encodeToJsonElement(origin).jsonObject["online"]!!.jsonObject["codec"]!!.jsonPrimitive.content.toInt() + ) // use custom serializer + + assertEquals( + "OnlineAudio", + format.encodeToJsonElement(origin).jsonObject["online"]!!.jsonObject["type"]!!.jsonPrimitive.content ) assertEquals( - v, - v.serialize(V.serializer()).deserialize(V.serializer()) + "OfflineAudio", + format.encodeToJsonElement(origin).jsonObject["offline"]!!.jsonObject["type"]!!.jsonPrimitive.content ) - 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()) + "OnlineAudio", + format.encodeToJsonElement(origin).jsonObject["onlineAsRec"]!!.jsonObject["type"]!!.jsonPrimitive.content ) assertEquals( - v, - v.serialize(V.serializer()).deserialize(V.serializer()) + "OfflineAudio", + format.encodeToJsonElement(origin).jsonObject["offlineAsRec"]!!.jsonObject["type"]!!.jsonPrimitive.content ) + + val result = origin.serialize().deserialize<AudioTestStandard>() + + assertEquals(origin.online::class, result.online::class) + assertEquals(origin.offline::class, result.offline::class) + assertEquals(origin.onlineAsRec::class, result.onlineAsRec::class) + assertEquals(origin.offlineAsRec::class, result.offlineAsRec::class) + + assertEquals(origin.online, result.online) + assertEquals(origin.offline, result.offline) + assertEquals(origin.onlineAsRec, result.onlineAsRec) + assertEquals(origin.offlineAsRec, result.offlineAsRec) + + assertEquals(origin, result) + } + + @Serializable + data class AudioTestWithPtt( + val online: OnlineAudio, + val offline: OfflineAudio, + val onlineAsRec: Audio, + val offlineAsRec: Audio, + ) + + @Test + fun `test Audio with ptt`() { + val origin = AudioTestWithPtt( + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, ImMsgBody.Ptt(1)), + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2)), + + OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, ImMsgBody.Ptt(1)), + OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2)), + ) + + val result = origin.serialize().deserialize<AudioTestWithPtt>() + + assertEquals(origin.online::class, result.online::class) + assertEquals(origin.offline::class, result.offline::class) + assertEquals(origin.onlineAsRec::class, result.onlineAsRec::class) + assertEquals(origin.offlineAsRec::class, result.offlineAsRec::class) + + assertEquals(origin.online, result.online) + assertEquals(origin.offline, result.offline) + assertEquals(origin.onlineAsRec, result.onlineAsRec) + assertEquals(origin.offlineAsRec, result.offlineAsRec) + + assertEquals(origin, result) } } \ No newline at end of file