From 5b3e508b75b4b56e10d4123b75d4bafa75a3110b Mon Sep 17 00:00:00 2001 From: StageGuard <45701251+StageGuard@users.noreply.github.com> Date: Sun, 20 Aug 2023 18:41:05 +0800 Subject: [PATCH] feature: add `ShortVideo` message support (#2739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial support for ShortVideo message * dump api * [core] upload protocol * [core] short video upload event * [core] doc * [core] protocol * [core] fix mp4 file check * [core] extract fileName from `OnlineShortVideo` to `ShortVideo` * [core] ShortVideo.Builder * [core] mirai code support for `ShortVideo` * [core] add doc for OnlineShortVideo and OfflineShortVideo * [core] fix text * dump api * update `Contact.uploadShortVideo`·` doc * [core] remove mirai code support for ShortVideo * [core] ensure Mirai service is loaded before load other services * [core] introduce `CombinedExternalResource` to reference multiple external resources for combined calculation. * [core] move refine context key defined in `OnlineShortVideoMsgInternal` to `RefineContext` * [core] remove data class * [core] broadcast `ShortVideoUploadEvent.Failed` event * [core] warn when cannot determine fromId * [core] add `contentToString` and `toString` for `OnlineShortVideoMsgInternal` * [core] optimize imports * [core] import * [core] revert * [core] doc * [core] auto close resource * dump api * keep consistence of param name * update doc * move Builder to OfflineShortVideo * optimize RefineContext * RefineContext.merge * dump api * fix test * show more video info * optimize constructor and builder of offline short video * optimize thumbnail * move thumbnail to main constructor arg * dump api * avoid null cast exception. * combine format transition * cleanup --- .../android/api/android.api | 96 +++++++ .../compatibility-validation/jvm/api/jvm.api | 96 +++++++ .../src/commonMain/kotlin/contact/Contact.kt | 22 +- .../event/events/ShortVideoUploadEvent.kt | 93 +++++++ .../commonMain/kotlin/message/data/Image.kt | 1 + .../kotlin/message/data/ShortVideo.kt | 250 ++++++++++++++++++ .../message/data/visitor/MessageVisitor.kt | 6 + .../kotlin/utils/ExternalResource.kt | 4 +- .../internal/contact/AbstractMockContact.kt | 13 +- .../src/commonMain/kotlin/Files.kt | 34 ++- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 4 +- .../kotlin/contact/AbstractContact.kt | 130 ++++++++- .../roaming/RoamingMessagesImplGroup.kt | 15 +- .../roaming/TimeBasedRoamingMessagesImpl.kt | 11 +- .../kotlin/message/ReceiveMessageHandler.kt | 37 ++- .../kotlin/message/RefinableMessage.kt | 43 +++ .../kotlin/message/data/shortVideo.kt | 243 +++++++++++++++++ .../image/InternalShortVideoProtocolImpl.kt | 41 +++ .../message/protocol/MessageProtocolFacade.kt | 6 +- .../protocol/decode/MessageDecoderPipeline.kt | 1 + .../protocol/impl/ShortVideoProtocol.kt | 98 +++++++ .../message/source/offlineSourceImpl.kt | 8 +- .../kotlin/network/highway/Highway.kt | 2 + .../notice/group/GroupMessageProcessor.kt | 28 +- .../notice/priv/PrivateMessageProcessor.kt | 15 +- .../protocol/data/proto/PttShortVideo.kt | 237 +++++++++++++++++ .../network/protocol/packet/PacketFactory.kt | 3 + .../packet/chat/shortvideo/PttCenterSvr.kt | 187 +++++++++++++ .../kotlin/utils/ExternalResourceImpl.kt | 15 ++ .../kotlin/utils/MiraiCoreServices.kt | 4 + ....internal.message.protocol.MessageProtocol | 1 + ...ai.message.data.InternalShortVideoProtocol | 10 + .../kotlin/message/RefineContextTest.kt | 55 ++++ .../kotlin/message/data/MessageRefineTest.kt | 9 +- .../protocol/MessageProtocolFacadeTest.kt | 1 + .../impl/AbstractMessageProtocolTest.kt | 9 +- .../kotlin/utils/ExternalResourceImpl.kt | 59 +++++ .../utils/CombinedExternalResourceTest.kt | 45 ++++ 38 files changed, 1893 insertions(+), 39 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/event/events/ShortVideoUploadEvent.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/message/data/ShortVideo.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/data/shortVideo.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/image/InternalShortVideoProtocolImpl.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/protocol/impl/ShortVideoProtocol.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/PttShortVideo.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/shortvideo/PttCenterSvr.kt create mode 100644 mirai-core/src/commonMain/kotlin/utils/ExternalResourceImpl.kt create mode 100644 mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.InternalShortVideoProtocol create mode 100644 mirai-core/src/commonTest/kotlin/message/RefineContextTest.kt create mode 100644 mirai-core/src/jvmBaseMain/kotlin/utils/ExternalResourceImpl.kt create mode 100644 mirai-core/src/jvmBaseTest/kotlin/utils/CombinedExternalResourceTest.kt diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index 97e66cd17b..0b72025f50 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -366,6 +366,10 @@ public abstract interface class net/mamoe/mirai/contact/Contact : kotlinx/corout public static fun uploadImage (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Image; public abstract fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadShortVideo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ShortVideo; + public abstract fun uploadShortVideo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadShortVideo$default (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/ShortVideo; + public static synthetic fun uploadShortVideo$default (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class net/mamoe/mirai/contact/Contact$Companion { @@ -1888,6 +1892,13 @@ public final class net/mamoe/mirai/event/events/BeforeImageUploadEvent : net/mam public fun toString ()Ljava/lang/String; } +public final class net/mamoe/mirai/event/events/BeforeShortVideoUploadEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/CancellableEvent, net/mamoe/mirai/event/events/BotActiveEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/event/VerboseEvent { + public fun getBot ()Lnet/mamoe/mirai/Bot; + public final fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public final fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public final fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; +} + public abstract interface class net/mamoe/mirai/event/events/BotActiveEvent : net/mamoe/mirai/event/events/BotEvent { } @@ -2948,6 +2959,30 @@ public final class net/mamoe/mirai/event/events/OtherClientOnlineEvent : net/mam public fun toString ()Ljava/lang/String; } +public abstract class net/mamoe/mirai/event/events/ShortVideoUploadEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/events/BotActiveEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/event/VerboseEvent { + public fun getBot ()Lnet/mamoe/mirai/Bot; + public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public abstract fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public abstract fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; +} + +public final class net/mamoe/mirai/event/events/ShortVideoUploadEvent$Failed : net/mamoe/mirai/event/events/ShortVideoUploadEvent { + public final fun getErrno ()I + public final fun getMessage ()Ljava/lang/String; + public fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/event/events/ShortVideoUploadEvent$Succeed : net/mamoe/mirai/event/events/ShortVideoUploadEvent { + public fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public final fun getVideo ()Lnet/mamoe/mirai/message/data/ShortVideo; + public fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun toString ()Ljava/lang/String; +} + public final class net/mamoe/mirai/event/events/SignEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/network/Packet { public fun getBot ()Lnet/mamoe/mirai/Bot; public final fun getRank ()Ljava/lang/Integer; @@ -4830,6 +4865,39 @@ 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/OfflineShortVideo : net/mamoe/mirai/message/data/ShortVideo { + public static final field Key Lnet/mamoe/mirai/message/data/OfflineShortVideo$Key; + public static final field SERIAL_NAME Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Builder { + public static final field Companion Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder$Companion; + public final fun build ()Lnet/mamoe/mirai/message/data/OfflineShortVideo; + public final fun getFileFormat ()Ljava/lang/String; + public final fun getFileMd5 ()[B + public final fun getFileName ()Ljava/lang/String; + public final fun getFileSize ()J + public final fun getThumbnailMd5 ()[B + public final fun getThumbnailSize ()J + public final fun getVideoId ()Ljava/lang/String; + public static final fun newBuilder (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder; + public final fun setFileFormat (Ljava/lang/String;)V + public final fun setFileMd5 ([B)V + public final fun setFileName (Ljava/lang/String;)V + public final fun setFileSize (J)V + public final fun setThumbnailMd5 ([B)V + public final fun setThumbnailSize (J)V + public final fun setVideoId (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Builder$Companion { + public final fun newBuilder (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder; +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + 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; @@ -4978,6 +5046,16 @@ public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/OnlineShortVideo : net/mamoe/mirai/message/data/ShortVideo { + public static final field Key Lnet/mamoe/mirai/message/data/OnlineShortVideo$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public abstract fun getUrlForDownload ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OnlineShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public final class net/mamoe/mirai/message/data/OrNullDelegate { public static final synthetic fun box-impl (Ljava/lang/Object;)Lnet/mamoe/mirai/message/data/OrNullDelegate; public static fun constructor-impl (Ljava/lang/Object;)Ljava/lang/Object; @@ -5223,6 +5301,24 @@ public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : ne public final class net/mamoe/mirai/message/data/ServiceMessage$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/ShortVideo : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/ShortVideo$Key; + public abstract fun getFileFormat ()Ljava/lang/String; + public abstract fun getFileMd5 ()[B + public abstract fun getFileSize ()J + public abstract fun getFilename ()Ljava/lang/String; + public fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public abstract fun getVideoId ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/ShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { +} + +public final class net/mamoe/mirai/message/data/ShortVideoKt { + public static final synthetic fun OfflineShortVideo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo; + public static synthetic fun OfflineShortVideo$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ[BJILjava/lang/Object;)Lnet/mamoe/mirai/message/data/OfflineShortVideo; +} + public final class net/mamoe/mirai/message/data/ShowImageFlag : net/mamoe/mirai/message/data/AbstractMessageKey, net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { public static final field INSTANCE Lnet/mamoe/mirai/message/data/ShowImageFlag; public static final field SERIAL_NAME Ljava/lang/String; diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index 67b41a3ddb..c5311a71a2 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -366,6 +366,10 @@ public abstract interface class net/mamoe/mirai/contact/Contact : kotlinx/corout public static fun uploadImage (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Image; public abstract fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadShortVideo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ShortVideo; + public abstract fun uploadShortVideo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadShortVideo$default (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/ShortVideo; + public static synthetic fun uploadShortVideo$default (Lnet/mamoe/mirai/contact/Contact;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class net/mamoe/mirai/contact/Contact$Companion { @@ -1888,6 +1892,13 @@ public final class net/mamoe/mirai/event/events/BeforeImageUploadEvent : net/mam public fun toString ()Ljava/lang/String; } +public final class net/mamoe/mirai/event/events/BeforeShortVideoUploadEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/CancellableEvent, net/mamoe/mirai/event/events/BotActiveEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/event/VerboseEvent { + public fun getBot ()Lnet/mamoe/mirai/Bot; + public final fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public final fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public final fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; +} + public abstract interface class net/mamoe/mirai/event/events/BotActiveEvent : net/mamoe/mirai/event/events/BotEvent { } @@ -2948,6 +2959,30 @@ public final class net/mamoe/mirai/event/events/OtherClientOnlineEvent : net/mam public fun toString ()Ljava/lang/String; } +public abstract class net/mamoe/mirai/event/events/ShortVideoUploadEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/events/BotActiveEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/event/VerboseEvent { + public fun getBot ()Lnet/mamoe/mirai/Bot; + public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public abstract fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public abstract fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; +} + +public final class net/mamoe/mirai/event/events/ShortVideoUploadEvent$Failed : net/mamoe/mirai/event/events/ShortVideoUploadEvent { + public final fun getErrno ()I + public final fun getMessage ()Ljava/lang/String; + public fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/event/events/ShortVideoUploadEvent$Succeed : net/mamoe/mirai/event/events/ShortVideoUploadEvent { + public fun getTarget ()Lnet/mamoe/mirai/contact/Contact; + public fun getThumbnailSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public final fun getVideo ()Lnet/mamoe/mirai/message/data/ShortVideo; + public fun getVideoSource ()Lnet/mamoe/mirai/utils/ExternalResource; + public fun toString ()Ljava/lang/String; +} + public final class net/mamoe/mirai/event/events/SignEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/event/events/BotEvent, net/mamoe/mirai/internal/network/Packet { public fun getBot ()Lnet/mamoe/mirai/Bot; public final fun getRank ()Ljava/lang/Integer; @@ -4830,6 +4865,39 @@ 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/OfflineShortVideo : net/mamoe/mirai/message/data/ShortVideo { + public static final field Key Lnet/mamoe/mirai/message/data/OfflineShortVideo$Key; + public static final field SERIAL_NAME Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Builder { + public static final field Companion Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder$Companion; + public final fun build ()Lnet/mamoe/mirai/message/data/OfflineShortVideo; + public final fun getFileFormat ()Ljava/lang/String; + public final fun getFileMd5 ()[B + public final fun getFileName ()Ljava/lang/String; + public final fun getFileSize ()J + public final fun getThumbnailMd5 ()[B + public final fun getThumbnailSize ()J + public final fun getVideoId ()Ljava/lang/String; + public static final fun newBuilder (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder; + public final fun setFileFormat (Ljava/lang/String;)V + public final fun setFileMd5 ([B)V + public final fun setFileName (Ljava/lang/String;)V + public final fun setFileSize (J)V + public final fun setThumbnailMd5 ([B)V + public final fun setThumbnailSize (J)V + public final fun setVideoId (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Builder$Companion { + public final fun newBuilder (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo$Builder; +} + +public final class net/mamoe/mirai/message/data/OfflineShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + 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; @@ -4978,6 +5046,16 @@ public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/OnlineShortVideo : net/mamoe/mirai/message/data/ShortVideo { + public static final field Key Lnet/mamoe/mirai/message/data/OnlineShortVideo$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public abstract fun getUrlForDownload ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/OnlineShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public final class net/mamoe/mirai/message/data/OrNullDelegate { public static final synthetic fun box-impl (Ljava/lang/Object;)Lnet/mamoe/mirai/message/data/OrNullDelegate; public static fun constructor-impl (Ljava/lang/Object;)Ljava/lang/Object; @@ -5223,6 +5301,24 @@ public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : ne public final class net/mamoe/mirai/message/data/ServiceMessage$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { } +public abstract interface class net/mamoe/mirai/message/data/ShortVideo : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/ShortVideo$Key; + public abstract fun getFileFormat ()Ljava/lang/String; + public abstract fun getFileMd5 ()[B + public abstract fun getFileSize ()J + public abstract fun getFilename ()Ljava/lang/String; + public fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public abstract fun getVideoId ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/ShortVideo$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { +} + +public final class net/mamoe/mirai/message/data/ShortVideoKt { + public static final synthetic fun OfflineShortVideo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ[BJ)Lnet/mamoe/mirai/message/data/OfflineShortVideo; + public static synthetic fun OfflineShortVideo$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BJ[BJILjava/lang/Object;)Lnet/mamoe/mirai/message/data/OfflineShortVideo; +} + public final class net/mamoe/mirai/message/data/ShowImageFlag : net/mamoe/mirai/message/data/AbstractMessageKey, net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { public static final field INSTANCE Lnet/mamoe/mirai/message/data/ShowImageFlag; public static final field SERIAL_NAME Ljava/lang/String; diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt index 56a0144866..7a0b16db28 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt @@ -72,8 +72,6 @@ public interface Contact : ContactOrBot, CoroutineScope { /** * 上传一个 [资源][ExternalResource] 作为图片以备发送. * - * **无论上传是否成功都不会关闭 [resource]. 需要调用方手动关闭资源** - * * 也可以使用其他扩展: [ExternalResource.uploadAsImage] 使用 [File], [InputStream] 等上传. * * @see Image 查看有关图片的更多信息, 如上传图片 @@ -88,6 +86,26 @@ public interface Contact : ContactOrBot, CoroutineScope { */ public suspend fun uploadImage(resource: ExternalResource): Image + /** + * 上传 [资源][ExternalResource] 作为短视频发送. + * 同时需要上传缩略图作为视频消息显示的封面. + * + * @see ShortVideo 查看有关短视频的更多信息 + * + * @see BeforeShortVideoUploadEvent 短视频发送前事件,可通过中断来拦截视频上传. + * @see ShortVideoUploadEvent 短视频上传完成事件,不可拦截. + * + * @param thumbnail 短视频封面图,为图片资源. + * @param video 视频资源,目前仅支持上传 mp4 格式的视频. + * @param fileName 文件名,若为 `null` 则根据 [video] 自动生成. + */ + public suspend fun uploadShortVideo( + thumbnail: ExternalResource, + video: ExternalResource, + fileName: String? = null + ): ShortVideo + + @JvmBlockingBridge public companion object { /** * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/ShortVideoUploadEvent.kt b/mirai-core-api/src/commonMain/kotlin/event/events/ShortVideoUploadEvent.kt new file mode 100644 index 0000000000..e8575cf6ce --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/event/events/ShortVideoUploadEvent.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2023 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:JvmMultifileClass +@file:JvmName("BotEventsKt") + +package net.mamoe.mirai.event.events + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.event.AbstractEvent +import net.mamoe.mirai.event.CancellableEvent +import net.mamoe.mirai.event.events.ShortVideoUploadEvent.Failed +import net.mamoe.mirai.event.events.ShortVideoUploadEvent.Succeed +import net.mamoe.mirai.internal.event.VerboseEvent +import net.mamoe.mirai.message.data.ShortVideo +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.MiraiInternalApi + + +/** + * 短视频上传前. 可以阻止上传. + * + * 此事件总是在 [ShortVideoUploadEvent] 之前广播. + * 若此事件被取消, [ShortVideoUploadEvent] 不会广播. + * + * @see Contact.uploadShortVideo 上传短视频. 为广播这个事件的唯一途径 + * @since 2.16 + */ +@OptIn(MiraiInternalApi::class) +public class BeforeShortVideoUploadEvent @MiraiInternalApi constructor( + public val target: Contact, + public val thumbnailSource: ExternalResource, + public val videoSource: ExternalResource +) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent, VerboseEvent { + public override val bot: Bot + get() = target.bot +} + +/** + * 短视频上传完成. + * + * 此事件总是在 [BeforeImageUploadEvent] 之后广播. + * 若 [BeforeImageUploadEvent] 被取消, 此事件不会广播. + * + * @see Contact.uploadShortVideo 上传短视频. 为广播这个事件的唯一途径 + * @see Succeed + * @see Failed + * @since 2.16 + */ +@OptIn(MiraiInternalApi::class) +public sealed class ShortVideoUploadEvent : BotEvent, BotActiveEvent, AbstractEvent(), VerboseEvent { + public abstract val target: Contact + public abstract val thumbnailSource: ExternalResource + public abstract val videoSource: ExternalResource + public override val bot: Bot + get() = target.bot + + public class Succeed @MiraiInternalApi constructor( + override val target: Contact, + override val thumbnailSource: ExternalResource, + override val videoSource: ExternalResource, + public val video: ShortVideo + ) : ShortVideoUploadEvent() { + override fun toString(): String { + return "ShortVideoUploadEvent.Succeed(target=$target, " + + "thumbnailSource=$thumbnailSource, " + + "videoSource=$videoSource, " + + "video=$video)" + } + } + + public class Failed @MiraiInternalApi constructor( + override val target: Contact, + override val thumbnailSource: ExternalResource, + override val videoSource: ExternalResource, + public val errno: Int, + public val message: String + ) : ShortVideoUploadEvent() { + override fun toString(): String { + return "ShortVideoUploadEvent.Failed(target=$target, " + + "thumbnailSource=$thumbnailSource, " + + "videoSource=$videoSource, " + + "errno=$errno, message='$message')" + } + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt index c84286ba90..8eb5b15236 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/Image.kt @@ -572,6 +572,7 @@ public interface InternalImageProtocol { // naming it Internal* to assign it a l @MiraiInternalApi public companion object { public val instance: InternalImageProtocol by lazy { + Mirai // initialize MiraiImpl first loadService( InternalImageProtocol::class, "net.mamoe.mirai.internal.message.InternalImageProtocolImpl" diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/ShortVideo.kt b/mirai-core-api/src/commonMain/kotlin/message/data/ShortVideo.kt new file mode 100644 index 0000000000..f6a4bde8fc --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/ShortVideo.kt @@ -0,0 +1,250 @@ +/* + * Copyright 2019-2023 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.message.data + +import kotlinx.serialization.KSerializer +import net.mamoe.mirai.Mirai +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.message.data.visitor.MessageVisitor +import net.mamoe.mirai.utils.* + +/** + * 短视频消息, 指的是可在聊天界面在线播放的视频消息, 而非在群文件上传的视频文件. + * + * 短视频消息分为 [OnlineShortVideo] 与 [OfflineShortVideo]. 在本地上传的短视频为 [OfflineShortVideo]. 从服务器接收的短视频为 [OnlineShortVideo]. + * + * 最推荐存储的方式是下载视频文件, 每次都通过上传该文件获取视频消息. + * 在上传视频时服务器会根据缓存情况选择回复已有视频 ID 或要求客户端上传. + * + * # 获取短视频消息示例 + * + * ## 上传短视频 + * 使用 [Contact.uploadShortVideo], 将视频缩略图和视频[资源][ExternalResource] 上传以得到 [OfflineShortVideo]. + * + * ## 使用 [OfflineShortVideo.Builder] 构建短视频 + * [OfflineShortVideo] 提供 [Builder][OfflineShortVideo.Builder] 构建方式, 必须指定 [videoId], [filename], [fileMd5], [fileSize] 和 [fileFormat] 参数. + * 可选指定 [thumbnailMd5][OfflineShortVideo.Builder.thumbnailMd5] 和 [thumbnailSize][OfflineShortVideo.Builder.thumbnailSize]. 若不提供, 可能会影响服务器判断缓存. + * + * ## 从服务器接收 + * 通过监听消息接收的短视频消息可直接转换为 [OnlineShortVideo]. + * + * kotlin 示例: + * ```kotlin + * val video: OnlineShortVideo = event.message[OnlineShortVideo] + * ``` + * + * # 下载视频 + * 通过 [OnlineShortVideo.urlForDownload] 获取下载链接. + * 该下载链接不包含短视频的文件信息, 可以使用 [videoId] 或 [filename] 作为文件名, [fileFormat] 作为文件拓展名. + * + * @since 2.16 + */ +@NotStableForInheritance +public interface ShortVideo : MessageContent, ConstrainSingle { + /** + * 视频 ID. + */ + public val videoId: String + + /** + * 视频文件 MD5. 16 bytes. + */ + public val fileMd5: ByteArray + + /* + * 视频大小 + */ + public val fileSize: Long + + /** + * 视频文件类型(拓展名) + */ + public val fileFormat: String + + /* + * 视频文件名, 不包括拓展名 + */ + public val filename: String + + + @MiraiInternalApi + override fun accept(visitor: MessageVisitor, data: D): R { + return visitor.visitShortVideo(this, data) + } + + override val key: MessageKey<*> + get() = Key + + + public companion object Key : + AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { + + } +} + +/** + * 在线短视频消息, 即从消息事件中接收到的视频消息. + * + * [OnlineShortVideo] 仅可以从事件中的[消息链][MessageChain]接收, 不可手动构造. + * + * ### 序列化支持 + * + * [OnlineShortVideo] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString]. + * 也可以在 [MessageSerializers.serializersModule] 获取到 [OnlineShortVideo] 的 [KSerializer]. + * + * 要获取更多有关序列化的信息, 参阅 [MessageSerializers]. + * + * @since 2.16 + */ +@NotStableForInheritance +public interface OnlineShortVideo : ShortVideo { + /** + * 下载链接 + */ + public val urlForDownload: String + + public companion object Key : + AbstractPolymorphicMessageKey(ShortVideo, { it.safeCast() }) { + public const val SERIAL_NAME: String = "OnlineShortVideo" + } +} + +/** + * 离线短视频消息. + * + * [OfflineShortVideo] 拥有协议上必要的五个属性: + * - 视频 ID [videoId] + * - 视频文件名 [filename] + * - 视频 MD5 [fileMd5] + * - 视频大小 [fileSize] + * - 视频格式 [fileFormat] + * + * 和非必要属性: + * - 缩略图 MD5 `thumbnailMd5` + * - 缩略图大小 `thumbnailSize` + * + * [OfflineShortVideo] 可由本地 [ExternalResource] 经过 [AudioSupported.uploadShortVideo] 上传到服务器得到, 故无[下载链接][OnlineShortVideo.urlForDownload]. + * + * [OfflineShortVideo] 支持使用 [OfflineShortVideo.Builder] 可通过上述七个必要参数和两个非必要参数构造 [OfflineShortVideo] 实例. + * + * ### 序列化支持 + * + * [OfflineShortVideo] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString]. + * 也可以在 [MessageSerializers.serializersModule] 获取到 [OfflineShortVideo] 的 [KSerializer]. + * + * 要获取更多有关序列化的信息, 参阅 [MessageSerializers]. + * @since 2.16 + */ +@NotStableForInheritance +public interface OfflineShortVideo : ShortVideo { + + public companion object Key : + AbstractPolymorphicMessageKey(ShortVideo, { it.safeCast() }) { + public const val SERIAL_NAME: String = "OfflineShortVideo" + } + + public class Builder internal constructor( + public var videoId: String, + public var fileMd5: ByteArray, + public var fileSize: Long, + public var fileFormat: String, + public var fileName: String + ) { + /** + * 缩略图文件 MD5 + * + * 传入此处的缩略图 MD5 应该仅有以下来源: + * * *已通过 [Contact.uploadShortVideo] 上传完成的*缩略图[资源][ExternalResource], 可由 [ExternalResource.md5] 获得. + */ + public var thumbnailMd5: ByteArray = EMPTY_BYTE_ARRAY + + /** + * 缩略图文件大小 + * + * 传入此处的缩略图文件大小应该仅有以下来源: + * * *已通过 [Contact.uploadShortVideo] 上传完成的*缩略图[资源][ExternalResource], 可由 [ExternalResource.size] 获得. + */ + public var thumbnailSize: Long = 0 + + public fun build(): OfflineShortVideo { + + @OptIn(MiraiInternalApi::class) + return InternalShortVideoProtocol.instance.createOfflineShortVideo( + videoId, fileMd5, fileSize, fileFormat, fileName, thumbnailMd5, thumbnailSize + ) + } + + public companion object { + /** + * 创建一个 [OfflineShortVideo.Builder] + * + * 在 Kotlin 可以使用类构造器的函数 [OfflineShortVideo]: `OfflineShortVideo(...)` + */ + @JvmStatic + public fun newBuilder( + videoId: String, + fileName: String, + fileFormat: String, + fileMd5: ByteArray, + fileSize: Long + ): Builder = Builder(videoId, fileMd5, fileSize, fileFormat, fileName) + } + } +} + +/** + * 构造 [OfflineShortVideo]. 有关参数的含义, 参考 [ShortVideo]. + * @since 2.16 + */ +@Suppress("NOTHING_TO_INLINE") +@JvmSynthetic +public inline fun OfflineShortVideo( + videoId: String, + fileName: String, + fileFormat: String, + fileMd5: ByteArray, + fileSize: Long, + thumbnailMd5: ByteArray = byteArrayOf(), + thumbnailSize: Long = 0, +): OfflineShortVideo = OfflineShortVideo.Builder.newBuilder(videoId, fileName, fileFormat, fileMd5, fileSize).apply { + this@apply.thumbnailMd5 = thumbnailMd5 + this@apply.thumbnailSize = thumbnailSize +}.build() + +/** + * 内部短视频协议实现, 请不要使用此接口 + * @since 2.16.0 + */ +@MiraiInternalApi +public interface InternalShortVideoProtocol { + public fun createOfflineShortVideo( + videoId: String, + fileMd5: ByteArray, + fileSize: Long, + fileFormat: String, + fileName: String, + thumbnailMd5: ByteArray, + thumbnailSize: Long + ): OfflineShortVideo + + @MiraiInternalApi + public companion object { + public val instance: InternalShortVideoProtocol by lazy { + Mirai // initialize MiraiImpl first + loadService( + InternalShortVideoProtocol::class, + "net.mamoe.mirai.internal.message.InternalShortVideoProtocolImpl" + ) + } + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt b/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt index 3c2b633918..de1bae7905 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt @@ -41,6 +41,8 @@ public interface MessageVisitor { public fun visitVoice(message: net.mamoe.mirai.message.data.Voice, data: D): R public fun visitAudio(message: Audio, data: D): R + public fun visitShortVideo(message: ShortVideo, data: D): R + // region HummerMessage public fun visitHummerMessage(message: HummerMessage, data: D): R public fun visitFlashImage(message: FlashImage, data: D): R @@ -164,6 +166,10 @@ public abstract class AbstractMessageVisitor : MessageVisitor return visitMessageContent(message, data) } + override fun visitShortVideo(message: ShortVideo, data: D): R { + return visitMessageContent(message, data) + } + public override fun visitHummerMessage(message: HummerMessage, data: D): R { return visitMessageContent(message, data) } diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index 383b43cc86..15074dfa8a 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -161,7 +161,9 @@ public interface ExternalResource : java.io.Closeable { * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME]. * * 默认会从文件头识别, 支持的文件类型: - * png, jpg, gif, tif, bmp, amr, silk + * * 图片类型: png, jpg, gif, tif, bmp + * * 语音类型: amr, silk + * * 视频类类型: mp4, mkv * * @see net.mamoe.mirai.utils.getFileType * @see net.mamoe.mirai.utils.FILE_TYPES diff --git a/mirai-core-mock/src/internal/contact/AbstractMockContact.kt b/mirai-core-mock/src/internal/contact/AbstractMockContact.kt index de810a9c3a..8be6eff2a1 100644 --- a/mirai-core-mock/src/internal/contact/AbstractMockContact.kt +++ b/mirai-core-mock/src/internal/contact/AbstractMockContact.kt @@ -16,10 +16,7 @@ import net.mamoe.mirai.event.events.MessagePreSendEvent import net.mamoe.mirai.internal.contact.broadcastMessagePreSendEvent import net.mamoe.mirai.internal.contact.replaceMagicCodes import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.OnlineMessageSource +import net.mamoe.mirai.message.data.* import net.mamoe.mirai.mock.MockBot import net.mamoe.mirai.mock.contact.MockContact import net.mamoe.mirai.utils.* @@ -61,6 +58,14 @@ internal abstract class AbstractMockContact( return bot.uploadMockImage(resource) } + override suspend fun uploadShortVideo( + thumbnail: ExternalResource, + video: ExternalResource, + fileName: String? + ): ShortVideo { + TODO("mock upload short video") + } + override fun toString(): String { return "$id" } diff --git a/mirai-core-utils/src/commonMain/kotlin/Files.kt b/mirai-core-utils/src/commonMain/kotlin/Files.kt index 29ffd2ecec..59cc1f1072 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Files.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Files.kt @@ -15,27 +15,35 @@ package net.mamoe.mirai.utils import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName +private class FileType( + signature: String, + val requiredHeaderSize: Int, + val formatName: String +) { + val signatureRegex = Regex(signature, RegexOption.IGNORE_CASE) +} + /** * 文件头和文件类型列表 */ -public val FILE_TYPES: MutableMap = mutableMapOf( - "FFD8FF" to "jpg", - "89504E47" to "png", - "47494638" to "gif", +private val FILE_TYPES: List = listOf( + FileType("^FFD8FF", 3, "jpg"), + FileType("^89504E47", 4, "png"), + FileType("^47494638", 4, "gif"), + FileType("^424D", 3, "bmp"), + FileType("^2321414D52", 5, "amr"), + FileType("^02232153494C4B5F5633", 10, "silk"), + FileType("^([a-zA-Z0-9]{8})66747970", 8, "mp4"), + //"49492A00" to "tif", // client doesn't support - "424D" to "bmp", //"52494646" to "webp", // pc client doesn't support - // "57415645" to "wav", // server doesn't support - - "2321414D52" to "amr", - "02232153494C4B5F5633" to "silk", ) /** * 在 [getFileType] 需要的 [ByteArray] 长度 */ -public val COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE: Int get() = FILE_TYPES.maxOf { it.key.length / 2 } +public val COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE: Int by lazy { FILE_TYPES.maxOf { it.requiredHeaderSize } } /* @@ -53,9 +61,9 @@ public fun getFileType(fileHeader: ByteArray): String? { "", length = COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE.coerceAtMost(fileHeader.size) ) - FILE_TYPES.forEach { (k, v) -> - if (hex.startsWith(k)) { - return v + FILE_TYPES.forEach { t -> + if (hex.contains(t.signatureRegex)) { + return t.formatName } } return null diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 2b1ef2025d..926cfad0fc 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -723,9 +723,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { it.fileName to it.buffer.loadAs(MsgTransmit.PbMultiMsgNew.serializer()) } val main = pbs["MultiMsg"] ?: return this.msg.map { it.toNode(bot, EmptyRefineContext) } - val context = SimpleRefineContext(mutableMapOf()) - context[ForwardMessageInternal.MsgTransmits] = pbs - return main.toForwardMessageNodes(bot, context) + return main.toForwardMessageNodes(bot, SimpleRefineContext(ForwardMessageInternal.MsgTransmits to pbs)) } private suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node { diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt index 7d67d1968a..dfb58c74da 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt @@ -9,10 +9,26 @@ package net.mamoe.mirai.internal.contact +import io.ktor.utils.io.core.* import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* +import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.event.events.BeforeShortVideoUploadEvent +import net.mamoe.mirai.event.events.EventCancelledException +import net.mamoe.mirai.event.events.ShortVideoUploadEvent import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.utils.childScopeContext +import net.mamoe.mirai.internal.message.data.OfflineShortVideoImpl +import net.mamoe.mirai.internal.message.data.ShortVideoThumbnail +import net.mamoe.mirai.internal.message.image.calculateImageInfo +import net.mamoe.mirai.internal.network.highway.Highway +import net.mamoe.mirai.internal.network.highway.ResourceKind +import net.mamoe.mirai.internal.network.protocol.data.proto.PttShortVideo +import net.mamoe.mirai.internal.network.protocol.packet.chat.video.PttCenterSvr +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf +import net.mamoe.mirai.message.data.ShortVideo +import net.mamoe.mirai.internal.utils.CombinedExternalResource +import net.mamoe.mirai.utils.* import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext @@ -21,6 +37,118 @@ internal abstract class AbstractContact( parentCoroutineContext: CoroutineContext, ) : Contact { final override val coroutineContext: CoroutineContext = parentCoroutineContext.childScopeContext() + + override suspend fun uploadShortVideo( + thumbnail: ExternalResource, + video: ExternalResource, + fileName: String? + ): ShortVideo = thumbnail.withAutoClose { + video.withAutoClose { + if (this !is Group && this !is Friend) { + throw UnsupportedOperationException("short video can only upload to friend or group.") + } + + if (video.formatName != "mp4") { + throw UnsupportedOperationException("video format ${video.formatName} is not supported.") + } + + if (BeforeShortVideoUploadEvent(this, thumbnail, video).broadcast().isCancelled) { + throw EventCancelledException("cancelled by BeforeShortVideoUploadEvent") + } + + // local uploaded offline short video uses video file md5 as its file name by default + val videoName = fileName ?: video.md5.toUHexString("") + + val uploadResp = bot.network.sendAndExpect( + PttCenterSvr.GroupShortVideoUpReq( + client = bot.client, + contact = this, + thumbnailFileMd5 = thumbnail.md5, + thumbnailFileSize = thumbnail.size, + videoFileName = videoName, + videoFileMd5 = video.md5, + videoFileSize = video.size, + videoFileFormat = video.formatName + ) + ) + + // get thumbnail image width and height + val thumbnailInfo = thumbnail.calculateImageInfo() + + // fast path + if (uploadResp is PttCenterSvr.GroupShortVideoUpReq.Response.FileExists) { + return OfflineShortVideoImpl( + uploadResp.fileId, + videoName, + video.md5, + video.size, + video.formatName, + ShortVideoThumbnail( + thumbnail.md5, + thumbnail.size, + thumbnailInfo.width, + thumbnailInfo.height + ) + ).also { + ShortVideoUploadEvent.Succeed(this, thumbnail, video, it).broadcast() + } + } + + val highwayRespExt = CombinedExternalResource(thumbnail, video).use { resource -> + Highway.uploadResourceBdh( + bot = bot, + resource = resource, + kind = ResourceKind.SHORT_VIDEO, + commandId = 25, + extendInfo = buildPacket { + writeProtoBuf( + PttShortVideo.PttShortVideoUploadReq.serializer(), + PttCenterSvr.GroupShortVideoUpReq.buildShortVideoFileInfo( + client = bot.client, + contact = this@AbstractContact, + thumbnailFileMd5 = thumbnail.md5, + thumbnailFileSize = thumbnail.size, + videoFileName = videoName, + videoFileMd5 = video.md5, + videoFileSize = video.size, + videoFileFormat = video.formatName + ) + ) + }.readBytes(), + encrypt = true + ).extendInfo + } + + if (highwayRespExt == null) { + ShortVideoUploadEvent.Failed( + this, + thumbnail, + video, + -1, + "highway upload short video failed, extendInfo is null." + ).broadcast() + error("highway upload short video failed, extendInfo is null.") + } + + val highwayUploadResp = highwayRespExt.loadAs(PttShortVideo.PttShortVideoUploadResp.serializer()) + + OfflineShortVideoImpl( + highwayUploadResp.fileid, + videoName, + video.md5, + video.size, + video.formatName, + ShortVideoThumbnail( + thumbnail.md5, + thumbnail.size, + thumbnailInfo.width, + thumbnailInfo.height + ) + ).also { + ShortVideoUploadEvent.Succeed(this, thumbnail, video, it).broadcast() + } + } + } } internal val Contact.userIdOrNull: Long? get() = if (this is User) this.id else null diff --git a/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImplGroup.kt b/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImplGroup.kt index 4745b29908..b73fdbac06 100644 --- a/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImplGroup.kt +++ b/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImplGroup.kt @@ -12,6 +12,8 @@ package net.mamoe.mirai.internal.contact.roaming import kotlinx.coroutines.flow.* import net.mamoe.mirai.contact.roaming.RoamingMessageFilter import net.mamoe.mirai.internal.contact.CommonGroupImpl +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement @@ -65,7 +67,18 @@ internal class RoamingMessagesImplGroup( .sortedByDescending { it.msgHead.msgSeq } // Ensure caller receives newer messages first .filter { filter.apply(it) } // Call filter after sort .asFlow() - .map { listOf(it).toMessageChainOnline(bot, contact.id, MessageSourceKind.GROUP) } + .map { + listOf(it).toMessageChainOnline( + bot, + contact.id, + MessageSourceKind.GROUP, + SimpleRefineContext( + RefineContextKey.MessageSourceKind to MessageSourceKind.GROUP, + RefineContextKey.FromId to it.msgHead.fromUin, + RefineContextKey.GroupIdOrZero to contact.uin, + ) + ) + } ) currentSeq = resp.msgElem.first().msgHead.msgSeq diff --git a/mirai-core/src/commonMain/kotlin/contact/roaming/TimeBasedRoamingMessagesImpl.kt b/mirai-core/src/commonMain/kotlin/contact/roaming/TimeBasedRoamingMessagesImpl.kt index 16bf47b79d..05661b4470 100644 --- a/mirai-core/src/commonMain/kotlin/contact/roaming/TimeBasedRoamingMessagesImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/roaming/TimeBasedRoamingMessagesImpl.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import net.mamoe.mirai.contact.roaming.RoamingMessageFilter +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetRoamMsgReq import net.mamoe.mirai.message.data.MessageChain @@ -32,13 +34,18 @@ internal sealed class TimeBasedRoamingMessagesImpl : AbstractRoamingMessages() { while (currentCoroutineContext().isActive) { val resp = requestRoamMsg(timeStart, lastMessageTime, random) val messages = resp.messages ?: break + if (filter == null || filter === RoamingMessageFilter.ANY) { // fast path - messages.forEach { emit(it.toMessageChainOnline(contact.bot)) } + messages.forEach { msg -> + val context = SimpleRefineContext(RefineContextKey.FromId to msg.msgHead.fromUin) + emit(msg.toMessageChainOnline(contact.bot, context)) + } } else { for (message in messages) { if (filter.invoke(createRoamingMessage(message, messages))) { - emit(message.toMessageChainOnline(contact.bot)) + val context = SimpleRefineContext(RefineContextKey.FromId to message.msgHead.fromUin) + emit(message.toMessageChainOnline(contact.bot, context)) } } } diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index f968140ace..4ad63f9393 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -24,8 +24,10 @@ import net.mamoe.mirai.internal.message.source.* import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.castOrNull import net.mamoe.mirai.utils.structureToString import net.mamoe.mirai.utils.toLongUnsigned +import net.mamoe.mirai.utils.warning /** * 只在手动构造 [OfflineMessageSource] 时调用 @@ -78,7 +80,17 @@ internal suspend fun MsgComm.Msg.toMessageChainOnline( MessageSourceKind.GROUP -> msgHead.groupInfo?.groupCode ?: 0 else -> 0 } - return listOf(this).toMessageChainOnline(bot, groupId, kind, refineContext, facade) + + return listOf(this).toMessageChainOnline( + bot, + groupId, + kind, + refineContext.merge(SimpleRefineContext( + RefineContextKey.MessageSourceKind to kind, + RefineContextKey.GroupIdOrZero to groupId + ), false), + facade + ) } //internal fun List.toMessageChainOffline( @@ -129,13 +141,28 @@ private fun List.toMessageChainImpl( val builder = MessageChainBuilder(messageList.sumOf { it.msgBody.richText.elems.size }) - if (onlineSource != null) { - builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList)) - } + val source = if (onlineSource != null) { + ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList) + } else null + if (source != null) builder.add(source) + val fromId = source?.fromId ?: firstOrNull()?.msgHead?.fromUin + if (fromId == null) { + bot.logger.warning { + "Cannot determine fromId from message source and msg elements, " + + "source: $source, elements: ${this.joinToString(", ")}" + } + } messageList.forEach { msg -> - facade.decode(msg.msgBody.richText.elems, groupIdOrZero, messageSourceKind, bot, builder, msg) + facade.decode( + msg.msgBody.richText.elems, + groupIdOrZero, + messageSourceKind, + bot, + builder, + msg + ) } for (msg in messageList) { diff --git a/mirai-core/src/commonMain/kotlin/message/RefinableMessage.kt b/mirai-core/src/commonMain/kotlin/message/RefinableMessage.kt index 41db4c07ce..05e5fecfdf 100644 --- a/mirai-core/src/commonMain/kotlin/message/RefinableMessage.kt +++ b/mirai-core/src/commonMain/kotlin/message/RefinableMessage.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.internal.message.LightMessageRefiner.refineMessageSource import net.mamoe.mirai.internal.message.flags.InternalFlagOnlyMessage import net.mamoe.mirai.internal.message.source.IncomingMessageSourceInternal import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.cast import net.mamoe.mirai.utils.safeCast /** @@ -99,6 +100,12 @@ internal class RefineContextKey( append(')') } } + + internal companion object { + val MessageSourceKind = RefineContextKey("MessageSourceKind") + val FromId = RefineContextKey("FromId") + val GroupIdOrZero = RefineContextKey("GroupIdOrZero") + } } /** @@ -108,6 +115,8 @@ internal interface RefineContext { operator fun contains(key: RefineContextKey<*>): Boolean operator fun get(key: RefineContextKey): T? fun getNotNull(key: RefineContextKey): T = get(key) ?: error("No such value of `$key`") + fun merge(other: RefineContext, override: Boolean): RefineContext + fun entries(): Set, Any>> } internal interface MutableRefineContext : RefineContext { @@ -118,9 +127,19 @@ internal interface MutableRefineContext : RefineContext { internal object EmptyRefineContext : RefineContext { override fun contains(key: RefineContextKey<*>): Boolean = false override fun get(key: RefineContextKey): T? = null + override fun merge(other: RefineContext, override: Boolean): RefineContext { + return other + } + override fun entries(): Set, Any>> { + return emptySet() + } override fun toString(): String { return "EmptyRefineContext" } + + override fun equals(other: Any?): Boolean { + return other === EmptyRefineContext + } } @Suppress("UNCHECKED_CAST") @@ -140,8 +159,32 @@ internal class SimpleRefineContext( override fun remove(key: RefineContextKey<*>) { delegate.remove(key) } + + override fun entries(): Set, Any>> { + return delegate.entries.map { (k, v) -> k to v }.toSet() + } + + override fun merge(other: RefineContext, override: Boolean): RefineContext { + val new = SimpleRefineContext(*entries().toTypedArray()) + other.entries().forEach { (key, value) -> + if (new[key] == null || override) { + new[key as RefineContextKey] = value + } + } + return new + } + + override fun equals(other: Any?): Boolean { + if (other !is RefineContext) return false + if (other === this) return true + + return other.entries() == entries() + } } +internal fun SimpleRefineContext(vararg elements: Pair, Any>): SimpleRefineContext = + SimpleRefineContext(elements.toMap().toMutableMap()) + /** * 执行不需要 `suspend` 的 refine. 用于 [MessageSource.originalMessage]. */ diff --git a/mirai-core/src/commonMain/kotlin/message/data/shortVideo.kt b/mirai-core/src/commonMain/kotlin/message/data/shortVideo.kt new file mode 100644 index 0000000000..d54ae99c12 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/data/shortVideo.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2019-2023 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.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.getMember +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.message.RefinableMessage +import net.mamoe.mirai.internal.message.RefineContext +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.protocol.impl.ShortVideoProtocol +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.network.protocol.packet.chat.video.PttCenterSvr +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.toUHexString + +/** + * receive from pipeline and refine to [OnlineShortVideoImpl] + */ +internal class OnlineShortVideoMsgInternal( + private val videoFile: ImMsgBody.VideoFile +) : RefinableMessage { + + override fun tryRefine(bot: Bot, context: MessageChain, refineContext: RefineContext): Message? { + return null + } + + override suspend fun refine(bot: Bot, context: MessageChain, refineContext: RefineContext): Message? { + bot.asQQAndroidBot() + + val sourceKind = refineContext[RefineContextKey.MessageSourceKind] ?: return null + val fromId = refineContext[RefineContextKey.FromId] ?: return null + val groupId = refineContext[RefineContextKey.GroupIdOrZero] ?: return null + + val contact = when (sourceKind) { + net.mamoe.mirai.message.data.MessageSourceKind.FRIEND -> bot.getFriend(fromId) + net.mamoe.mirai.message.data.MessageSourceKind.GROUP -> bot.getGroup(groupId) + else -> return null // TODO: ignore processing stranger's video message + } as Contact + val sender = when (sourceKind) { + net.mamoe.mirai.message.data.MessageSourceKind.FRIEND -> + bot.getFriend(fromId) ?: error("Cannot find friend $fromId.") + net.mamoe.mirai.message.data.MessageSourceKind.GROUP -> { + val group = bot.getGroup(groupId) ?: error("Cannot find group $groupId.") + group.getMember(fromId) ?: error("Cannot find member $fromId of group $groupId.") + } + else -> return null // TODO: ignore processing stranger's video message + } + + val shortVideoDownloadReq = bot.network.sendAndExpect( + PttCenterSvr.ShortVideoDownReq( + bot.client, + contact, + sender, + videoFile.fileUuid.decodeToString(), + videoFile.fileMd5 + ) + ) + + if (shortVideoDownloadReq !is PttCenterSvr.ShortVideoDownReq.Response.Success) + throw IllegalStateException("Failed to query short video download attributes.") + + if (!shortVideoDownloadReq.fileMd5.contentEquals(videoFile.fileMd5)) + throw IllegalStateException( + "Queried short video download attributes doesn't match the requests. " + + "message provides: ${videoFile.fileMd5.toUHexString("")}, " + + "queried result: ${shortVideoDownloadReq.fileMd5.toUHexString("")}" + ) + + val format = ShortVideoProtocol.FORMAT + .firstOrNull { it.second == videoFile.fileFormat }?.first + ?: ExternalResource.DEFAULT_FORMAT_NAME + + return OnlineShortVideoImpl( + videoFile.fileUuid.decodeToString(), + shortVideoDownloadReq.fileMd5, + videoFile.fileName.decodeToString(), + videoFile.fileSize.toLong(), + format, + shortVideoDownloadReq.urlV4, + ShortVideoThumbnail( + videoFile.thumbFileMd5, + videoFile.thumbFileSize.toLong(), + videoFile.thumbWidth, + videoFile.thumbHeight + ) + ) + } + + + override fun toString(): String { + return "OnlineShortVideoMsgInternal(videoElem=$videoFile)" + } + + override fun contentToString(): String { + return "[视频元数据]" + } +} + +@Serializable +internal data class ShortVideoThumbnail( + val md5: ByteArray, + val size: Long, + val width: Int?, + val height: Int?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ShortVideoThumbnail + + if (!md5.contentEquals(other.md5)) return false + if (size != other.size) return false + if (width != other.width) return false + if (height != other.height) return false + + return true + } + + override fun hashCode(): Int { + var result = md5.contentHashCode() + result = 31 * result + size.hashCode() + result = 31 * result + (width ?: 0) + result = 31 * result + (height ?: 0) + return result + } +} + +internal abstract class AbstractShortVideoWithThumbnail : ShortVideo { + abstract val thumbnail: ShortVideoThumbnail +} + +@Suppress("DuplicatedCode") +@SerialName(OnlineShortVideo.SERIAL_NAME) +@Serializable +internal class OnlineShortVideoImpl( + override val videoId: String, + override val fileMd5: ByteArray, + override val filename: String, + override val fileSize: Long, + override val fileFormat: String, + override val urlForDownload: String, + override val thumbnail: ShortVideoThumbnail +) : OnlineShortVideo, AbstractShortVideoWithThumbnail() { + + override fun toString(): String { + return "[mirai:shortvideo:$videoId, videoName=$filename.$fileFormat, videoMd5=${fileMd5.toUHexString("")}, " + + "videoSize=${fileSize}, thumbnailMd5=${thumbnail.md5.toUHexString("")}, thumbnailSize=${thumbnail.size}]" + } + + override fun contentToString(): String { + return "[视频]" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OnlineShortVideoImpl + + if (videoId != other.videoId) return false + if (!fileMd5.contentEquals(other.fileMd5)) return false + if (filename != other.filename) return false + if (fileSize != other.fileSize) return false + if (fileFormat != other.fileFormat) return false + if (urlForDownload != other.urlForDownload) return false + if (thumbnail != other.thumbnail) return false + + return true + } + + override fun hashCode(): Int { + var result = videoId.hashCode() + result = 31 * result + fileMd5.contentHashCode() + result = 31 * result + filename.hashCode() + result = 31 * result + fileSize.hashCode() + result = 31 * result + fileFormat.hashCode() + result = 31 * result + urlForDownload.hashCode() + result = 31 * result + thumbnail.hashCode() + return result + } +} + +@Serializable +internal class OfflineShortVideoImpl( + override val videoId: String, + override val filename: String, + override val fileMd5: ByteArray, + override val fileSize: Long, + override val fileFormat: String, + override val thumbnail: ShortVideoThumbnail +) : OfflineShortVideo, AbstractShortVideoWithThumbnail() { + + /** + * offline short video uses + */ + override fun toString(): String { + return "[mirai:shortvideo:$videoId, videoName=$filename.$fileFormat, videoMd5=${fileMd5.toUHexString("")}, " + + "videoSize=${fileSize}, thumbnailMd5=${thumbnail.md5.toUHexString("")}, thumbnailSize=${thumbnail.size}]" + } + + override fun contentToString(): String { + return "[视频]" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OfflineShortVideoImpl + + if (videoId != other.videoId) return false + if (filename != other.filename) return false + if (!fileMd5.contentEquals(other.fileMd5)) return false + if (fileSize != other.fileSize) return false + if (fileFormat != other.fileFormat) return false + if (thumbnail != other.thumbnail) return false + + return true + } + + override fun hashCode(): Int { + var result = videoId.hashCode() + result = 31 * result + filename.hashCode() + result = 31 * result + fileMd5.contentHashCode() + result = 31 * result + fileSize.hashCode() + result = 31 * result + fileFormat.hashCode() + result = 31 * result + thumbnail.hashCode() + return result + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/image/InternalShortVideoProtocolImpl.kt b/mirai-core/src/commonMain/kotlin/message/image/InternalShortVideoProtocolImpl.kt new file mode 100644 index 0000000000..3849ebdccd --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/image/InternalShortVideoProtocolImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2023 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.image + +import net.mamoe.mirai.internal.message.data.OfflineShortVideoImpl +import net.mamoe.mirai.internal.message.data.ShortVideoThumbnail +import net.mamoe.mirai.message.data.InternalShortVideoProtocol +import net.mamoe.mirai.message.data.OfflineShortVideo + +internal class InternalShortVideoProtocolImpl : InternalShortVideoProtocol { + override fun createOfflineShortVideo( + videoId: String, + fileMd5: ByteArray, + fileSize: Long, + fileFormat: String, + fileName: String, + thumbnailMd5: ByteArray, + thumbnailSize: Long + ): OfflineShortVideo { + return OfflineShortVideoImpl( + videoId, + fileName, + fileMd5, + fileSize, + fileFormat, + ShortVideoThumbnail( + thumbnailMd5, + thumbnailSize, + 0, + 0 + ) + ) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt index 4220967880..5a96dfe220 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt @@ -135,7 +135,9 @@ internal interface MessageProtocolFacade { groupIdOrZero: Long, messageSourceKind: MessageSourceKind, bot: Bot, - ): MessageChain = buildMessageChain { decode(elements, groupIdOrZero, messageSourceKind, bot, this, null) } + ): MessageChain = buildMessageChain { + decode(elements, groupIdOrZero, messageSourceKind, bot, this, null) + } fun createSerializersModule(): SerializersModule = SerializersModule { @@ -336,6 +338,7 @@ internal class MessageProtocolFacadeImpl( return getSingleReceipt(result, message) } + override suspend fun preprocessAndSendOutgoing( target: C, message: Message, @@ -378,6 +381,7 @@ internal class MessageProtocolFacadeImpl( "Internal error: no MessageReceipt was returned from OutgoingMessagePipeline for message", forDebug = message.structureToString() ) + 1 -> return result.single().castUp() else -> throw contextualBugReportException( "Internal error: multiple MessageReceipts were returned from OutgoingMessagePipeline: $result", diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt index ef0a27c4de..6170a636ca 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt @@ -29,6 +29,7 @@ internal interface MessageDecoderContext : ProcessorPipelineContext("messageSourceKind") val GROUP_ID = TypeKey("groupId") // zero if not group val CONTAINING_MSG = TypeKey("containingMsg") + val FROM_ID = TypeKey("fromId") // group/temp = sender, friend/stranger = this } } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ShortVideoProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ShortVideoProtocol.kt new file mode 100644 index 0000000000..82b97da576 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ShortVideoProtocol.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2023 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.protocol.impl + +import net.mamoe.mirai.internal.message.data.AbstractShortVideoWithThumbnail +import net.mamoe.mirai.internal.message.data.OfflineShortVideoImpl +import net.mamoe.mirai.internal.message.data.OnlineShortVideoImpl +import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal +import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.ProcessorCollector +import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder +import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoderContext +import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoder +import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext +import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.message.data.MessageContent +import net.mamoe.mirai.message.data.ShortVideo +import net.mamoe.mirai.message.data.SingleMessage + +internal class ShortVideoProtocol : MessageProtocol() { + override fun ProcessorCollector.collectProcessorsImpl() { + add(Decoder()) + add(Encoder()) + + MessageSerializer.superclassesScope(ShortVideo::class, MessageContent::class, SingleMessage::class) { + add(MessageSerializer(OfflineShortVideoImpl::class, OfflineShortVideoImpl.serializer())) + add(MessageSerializer(OnlineShortVideoImpl::class, OnlineShortVideoImpl.serializer())) + } + } + + private class Decoder : MessageDecoder { + override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) { + val videoFile = data.videoFile ?: return + markAsConsumed() + + collect(OnlineShortVideoMsgInternal(videoFile)) + } + } + + private class Encoder : MessageEncoder { + override suspend fun MessageEncoderContext.process(data: AbstractShortVideoWithThumbnail) { + markAsConsumed() + + collect(ImMsgBody.Elem(text = ImMsgBody.Text("你的 QQ 暂不支持查看视频短片,请期待后续版本。"))) + + val thumbWidth = if (data.thumbnail.width == null || data.thumbnail.width == 0) 1280 else data.thumbnail.width!! + val thumbHeight = if (data.thumbnail.height == null || data.thumbnail.height == 0) 720 else data.thumbnail.height!! + + collect( + ImMsgBody.Elem( + videoFile = ImMsgBody.VideoFile( + fileUuid = data.videoId.encodeToByteArray(), + fileMd5 = data.fileMd5, + fileName = data.filename.encodeToByteArray(), + fileFormat = FORMAT.firstOrNull { it.first == data.fileFormat }?.second ?: 3, + fileTime = 10, + fileSize = data.fileSize.toInt(), + thumbWidth = thumbWidth, + thumbHeight = thumbHeight, + thumbFileMd5 = data.thumbnail.md5, + thumbFileSize = data.thumbnail.size.toInt(), + busiType = 0, + fromChatType = -1, + toChatType = -1, + boolSupportProgressive = true, + fileWidth = thumbWidth, + fileHeight = thumbHeight + ) + ) + ) + } + + } + + internal companion object { + internal val FORMAT: List> = listOf( + "ts" to 1, + "avi" to 2, + "mp4" to 3, + "wmv" to 4, + "mkv" to 5, + "rmvb" to 6, + "rm" to 7, + "afs" to 8, + "mov" to 9, + "mod" to 10, + "mts" to 11 + ) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt index b429c320ce..2a4e643ef5 100644 --- a/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/source/offlineSourceImpl.kt @@ -16,6 +16,8 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import net.mamoe.mirai.Bot import net.mamoe.mirai.internal.message.MessageSourceSerializerImpl +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.toMessageChainNoSource import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm @@ -183,7 +185,10 @@ internal fun OfflineMessageSourceImplData( internalIds = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()) .origUids?.mapToIntArray { it.toInt() } ?: intArrayOf(), time = delegate.time, - originalMessageLazy = lazy { delegate.toMessageChainNoSource(bot, messageSourceKind, groupIdOrZero) }, + originalMessageLazy = lazy { + val context = SimpleRefineContext(RefineContextKey.FromId to delegate.senderUin) + delegate.toMessageChainNoSource(bot, messageSourceKind, groupIdOrZero, context) + }, fromId = delegate.senderUin, targetId = when { groupIdOrZero != 0L -> groupIdOrZero @@ -191,6 +196,7 @@ internal fun OfflineMessageSourceImplData( delegate.srcMsg != null -> runCatching { delegate.srcMsg.loadAs(MsgComm.Msg.serializer()).msgHead.toUin }.getOrElse { 0L } + else -> 0/*error("cannot find targetId. delegate=${delegate._miraiContentToString()}, delegate.srcMsg=${ kotlin.runCatching { delegate.srcMsg?.loadAs(MsgComm.Msg.serializer())?._miraiContentToString() } .fold( diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index c8665cb4c0..44dc622162 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -126,6 +126,8 @@ internal enum class ResourceKind( FORWARD_MESSAGE("forward message"), ANNOUNCEMENT_IMAGE("announcement image"), + + SHORT_VIDEO("short video") ; override fun toString(): String = display diff --git a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt index 0d46b26bb3..cedaba8030 100644 --- a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt @@ -20,6 +20,8 @@ import net.mamoe.mirai.event.events.MemberCardChangeEvent import net.mamoe.mirai.event.events.MemberSpecialTitleChangeEvent import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.components.NoticePipelineContext @@ -159,7 +161,18 @@ internal class GroupMessageProcessor( GroupMessageSyncEvent( client = bot.otherClients.find { it.appId == msgHead.fromInstid } ?: return, // don't compare with dstAppId. diff. - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), + message = msgs.map { it.msg }.toMessageChainOnline( + bot, + group.id, + MessageSourceKind.GROUP, + SimpleRefineContext( + mutableMapOf( + RefineContextKey.MessageSourceKind to MessageSourceKind.GROUP, + RefineContextKey.FromId to sender.uin, + RefineContextKey.GroupIdOrZero to group.uin, + ) + ) + ), time = msgHead.msgTime, group = group, sender = sender, @@ -174,7 +187,18 @@ internal class GroupMessageProcessor( GroupMessageEvent( senderName = nameCard.nick, sender = sender, - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), + message = msgs.map { it.msg }.toMessageChainOnline( + bot, + group.id, + MessageSourceKind.GROUP, + SimpleRefineContext( + mutableMapOf( + RefineContextKey.MessageSourceKind to MessageSourceKind.GROUP, + RefineContextKey.FromId to sender.uin, + RefineContextKey.GroupIdOrZero to group.uin, + ) + ) + ), permission = sender.permission, time = msgHead.msgTime, ), diff --git a/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt index 99dadfc656..ba0a27a163 100644 --- a/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt @@ -15,6 +15,8 @@ import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.getGroupByUinOrCode +import net.mamoe.mirai.internal.message.RefineContextKey +import net.mamoe.mirai.internal.message.SimpleRefineContext import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.components.NoticePipelineContext @@ -25,6 +27,7 @@ import net.mamoe.mirai.internal.network.components.SsoProcessor import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore +import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.utils.assertUnreachable import net.mamoe.mirai.utils.context @@ -114,6 +117,7 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor(type val group = bot.getGroupByUinOrCode(tmpHead.groupUin) ?: return handlePrivateMessage(data, group[senderUin] ?: return) } + else -> markNotConsumed() } @@ -129,7 +133,16 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor(type val msgs = user.fragmentedMessageMerger.tryMerge(this) if (msgs.isEmpty()) return - val chain = msgs.toMessageChainOnline(bot, 0, user.correspondingMessageSourceKind) + val chain = msgs.toMessageChainOnline( + bot, + 0, + user.correspondingMessageSourceKind, + SimpleRefineContext( + RefineContextKey.MessageSourceKind to MessageSourceKind.FRIEND, + RefineContextKey.FromId to user.uin, + RefineContextKey.GroupIdOrZero to 0L, + ) + ) val time = msgHead.msgTime collected += if (fromSync) { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/PttShortVideo.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/PttShortVideo.kt new file mode 100644 index 0000000000..c6d8576c6e --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/PttShortVideo.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2019-2023 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.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.utils.io.ProtoBuf +import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY + +@Serializable +internal class PttShortVideo : ProtoBuf { + @Serializable + internal class ServerListInfo( + @JvmField @ProtoNumber(1) val upIp: Int = 0, + @JvmField @ProtoNumber(2) val upPort: Int = 0 + ) : ProtoBuf + + @Serializable + internal class CodecConfigReq( + @JvmField @ProtoNumber(1) val platformChipinfo: String = "", + @JvmField @ProtoNumber(2) val osVersion: String = "", + @JvmField @ProtoNumber(3) val deviceName: String = "" + ) : ProtoBuf + + @Serializable + internal class DataHole( + @JvmField @ProtoNumber(1) val begin: Long = 0L, + @JvmField @ProtoNumber(2) val end: Long = 0L + ) : ProtoBuf + + @Serializable + internal class ExtensionReq( + @JvmField @ProtoNumber(1) val subBusiType: Int = 0, + @JvmField @ProtoNumber(2) val userCnt: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoAddr( + @JvmField @ProtoNumber(1) val hostType: Int = 0, + @JvmField @ProtoNumber(10) val strHost: List = emptyList(), + @JvmField @ProtoNumber(11) val urlArgs: String = "", + @JvmField @ProtoNumber(21) val strHostIpv6: List = emptyList(), + @JvmField @ProtoNumber(22) val strDomain: List = emptyList() + ) : ProtoBuf + + @Serializable + internal class PttShortVideoDeleteReq( + @JvmField @ProtoNumber(1) val fromuin: Long = 0L, + @JvmField @ProtoNumber(2) val touin: Long = 0L, + @JvmField @ProtoNumber(3) val chatType: Int = 0, + @JvmField @ProtoNumber(4) val clientType: Int = 0, + @JvmField @ProtoNumber(5) val fileid: String = "", + @JvmField @ProtoNumber(6) val groupCode: Long = 0L, + @JvmField @ProtoNumber(7) val agentType: Int = 0, + @JvmField @ProtoNumber(8) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(9) val businessType: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoDeleteResp( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "" + ) : ProtoBuf + + @Serializable + internal class PttShortVideoDownloadReq( + @JvmField @ProtoNumber(1) val fromuin: Long = 0L, + @JvmField @ProtoNumber(2) val touin: Long = 0L, + @JvmField @ProtoNumber(3) val chatType: Int = 0, + @JvmField @ProtoNumber(4) val clientType: Int = 0, + @JvmField @ProtoNumber(5) val fileid: String = "", + @JvmField @ProtoNumber(6) val groupCode: Long = 0L, + @JvmField @ProtoNumber(7) val agentType: Int = 0, + @JvmField @ProtoNumber(8) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(9) val businessType: Int = 0, + @JvmField @ProtoNumber(10) val fileType: Int = 0, + @JvmField @ProtoNumber(11) val downType: Int = 0, + @JvmField @ProtoNumber(12) val sceneType: Int = 0, + @JvmField @ProtoNumber(13) val needInnerAddr: Int = 0, + @JvmField @ProtoNumber(14) val reqTransferType: Int = 0, + @JvmField @ProtoNumber(15) val reqHostType: Int = 0, + @JvmField @ProtoNumber(20) val flagSupportLargeSize: Int = 0, + @JvmField @ProtoNumber(30) val flagClientQuicProtoEnable: Int = 0, + @JvmField @ProtoNumber(31) val targetCodecFormat: Int = 0, + @JvmField @ProtoNumber(32) val msgCodecConfig: CodecConfigReq? = null, + @JvmField @ProtoNumber(33) val sourceCodecFormat: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoDownloadResp( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val sameAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(4) val diffAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(5) val downloadkey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(6) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(7) val sameAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(8) val diffAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(9) val msgDownloadAddr: PttShortVideoAddr? = null, + @JvmField @ProtoNumber(10) val encryptKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(30) val flagServerQuicProtoEnable: Int = 0, + @JvmField @ProtoNumber(31) val serverQuicPara: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(32) val codecFormat: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoFileInfo( + @JvmField @ProtoNumber(1) val fileName: String = "", + @JvmField @ProtoNumber(2) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(3) val thumbFileMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(4) val fileSize: Long = 0L, + @JvmField @ProtoNumber(5) val fileResLength: Int = 0, + @JvmField @ProtoNumber(6) val fileResWidth: Int = 0, + @JvmField @ProtoNumber(7) val fileFormat: Int = 0, + @JvmField @ProtoNumber(8) val fileTime: Int = 0, + @JvmField @ProtoNumber(9) val thumbFileSize: Long = 0L, + @JvmField @ProtoNumber(10) val decryptVideoMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(11) val decryptFileSize: Long = 0L, + @JvmField @ProtoNumber(12) val decryptThumbMd5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(13) val decryptThumbSize: Long = 0L, + @JvmField @ProtoNumber(14) val extend: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + internal class PttShortVideoFileInfoExtend( + @JvmField @ProtoNumber(1) val bitRate: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoIpList( + @JvmField @ProtoNumber(1) val ip: Int = 0, + @JvmField @ProtoNumber(2) val port: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoRetweetReq( + @JvmField @ProtoNumber(1) val fromUin: Long = 0L, + @JvmField @ProtoNumber(2) val toUin: Long = 0L, + @JvmField @ProtoNumber(3) val fromChatType: Int = 0, + @JvmField @ProtoNumber(4) val toChatType: Int = 0, + @JvmField @ProtoNumber(5) val fromBusiType: Int = 0, + @JvmField @ProtoNumber(6) val toBusiType: Int = 0, + @JvmField @ProtoNumber(7) val clientType: Int = 0, + @JvmField @ProtoNumber(8) val msgPttShortVideoFileInfo: PttShortVideoFileInfo? = null, + @JvmField @ProtoNumber(9) val agentType: Int = 0, + @JvmField @ProtoNumber(10) val fileid: String = "", + @JvmField @ProtoNumber(11) val groupCode: Long = 0L, + @JvmField @ProtoNumber(20) val flagSupportLargeSize: Int = 0, + @JvmField @ProtoNumber(21) val codecFormat: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoRetweetResp( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val sameAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(4) val diffAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(5) val fileid: String = "", + @JvmField @ProtoNumber(6) val ukey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(7) val fileExist: Int = 0, + @JvmField @ProtoNumber(8) val sameAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(9) val diffAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(10) val dataHole: List = emptyList(), + @JvmField @ProtoNumber(11) val isHotFile: Int = 0, + @JvmField @ProtoNumber(12) val longVideoCarryWatchPointType: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoUploadReq( + @JvmField @ProtoNumber(1) val fromuin: Long = 0L, + @JvmField @ProtoNumber(2) val touin: Long = 0L, + @JvmField @ProtoNumber(3) val chatType: Int = 0, + @JvmField @ProtoNumber(4) val clientType: Int = 0, + @JvmField @ProtoNumber(5) val msgPttShortVideoFileInfo: PttShortVideoFileInfo? = null, + @JvmField @ProtoNumber(6) val groupCode: Long = 0L, + @JvmField @ProtoNumber(7) val agentType: Int = 0, + @JvmField @ProtoNumber(8) val businessType: Int = 0, + @JvmField @ProtoNumber(9) val encryptKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(10) val subBusinessType: Int = 0, + @JvmField @ProtoNumber(20) val flagSupportLargeSize: Int = 0, + @JvmField @ProtoNumber(21) val codecFormat: Int = 0 + ) : ProtoBuf + + @Serializable + internal class PttShortVideoUploadResp( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val sameAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(4) val diffAreaOutAddr: List = emptyList(), + @JvmField @ProtoNumber(5) val fileid: String = "", + @JvmField @ProtoNumber(6) val ukey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(7) val fileExist: Int = 0, + @JvmField @ProtoNumber(8) val sameAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(9) val diffAreaInnerAddr: List = emptyList(), + @JvmField @ProtoNumber(10) val dataHole: List = emptyList(), + @JvmField @ProtoNumber(11) val encryptKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(12) val isHotFile: Int = 0, + @JvmField @ProtoNumber(13) val longVideoCarryWatchPointType: Int = 0 + ) : ProtoBuf + + @Serializable + internal class QuicParameter( + @JvmField @ProtoNumber(1) val enableQuic: Int = 0, + @JvmField @ProtoNumber(2) val encryptionVer: Int = 1, + @JvmField @ProtoNumber(3) val fecVer: Int = 0 + ) : ProtoBuf + + @Serializable + internal class ReqBody( + @JvmField @ProtoNumber(1) val cmd: Int = 0, + @JvmField @ProtoNumber(2) val seq: Int = 0, + @JvmField @ProtoNumber(3) val msgPttShortVideoUploadReq: PttShortVideoUploadReq? = null, + @JvmField @ProtoNumber(4) val msgPttShortVideoDownloadReq: PttShortVideoDownloadReq? = null, + @JvmField @ProtoNumber(5) val msgShortVideoRetweetReq: List = emptyList(), + @JvmField @ProtoNumber(6) val msgShortVideoDeleteReq: List = emptyList(), + @JvmField @ProtoNumber(100) val msgExtensionReq: List = emptyList() + ) : ProtoBuf + + @Serializable + internal class RspBody( + @JvmField @ProtoNumber(1) val cmd: Int = 0, + @JvmField @ProtoNumber(2) val seq: Int = 0, + @JvmField @ProtoNumber(3) val msgPttShortVideoUploadResp: PttShortVideoUploadResp? = null, + @JvmField @ProtoNumber(4) val msgPttShortVideoDownloadResp: PttShortVideoDownloadResp? = null, + @JvmField @ProtoNumber(5) val msgShortVideoRetweetResp: List = emptyList(), + @JvmField @ProtoNumber(6) val msgShortVideoDeleteResp: List = emptyList(), + @JvmField @ProtoNumber(100) val changeChannel: Int = 0, + @JvmField @ProtoNumber(101) val allowRetry: Int = 0 + ) : ProtoBuf +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index e0219ced6b..2c4d7a886c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.* +import net.mamoe.mirai.internal.network.protocol.packet.chat.video.PttCenterSvr import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService @@ -151,6 +152,8 @@ internal object KnownPacketFactories { PttStore.GroupPttUp, PttStore.GroupPttDown, PttStore.C2CPttDown, + PttCenterSvr.GroupShortVideoUpReq, + PttCenterSvr.ShortVideoDownReq, LongConn.OffPicUp, // LongConn.OffPicDown, TroopManagement.EditSpecialTitle, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/shortvideo/PttCenterSvr.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/shortvideo/PttCenterSvr.kt new file mode 100644 index 0000000000..8c5818d54f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/shortvideo/PttCenterSvr.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2023 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.network.protocol.packet.chat.video + +import io.ktor.utils.io.core.* +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.User +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.uin +import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.protocol.data.proto.PttShortVideo +import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory +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 + +internal class PttCenterSvr { + object GroupShortVideoUpReq : + OutgoingPacketFactory("PttCenterSvr.GroupShortVideoUpReq") { + sealed class Response : Packet { + class FileExists(val fileId: String) : Response() { + override fun toString(): String { + return "PttCenterSvr.GroupShortVideoUpReq.Response.FileExists(fileId=${fileId})" + } + } + + object RequireUpload : Response() { + override fun toString(): String { + return "PttCenterSvr.GroupShortVideoUpReq.Response.RequireUpload" + } + } + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { + val resp = readProtoBuf(PttShortVideo.RspBody.serializer()) + val upResp = resp.msgPttShortVideoUploadResp ?: return Response.RequireUpload + + return if (upResp.fileExist == 1) { + Response.FileExists(upResp.fileid) + } else { + Response.RequireUpload + } + } + + operator fun invoke( + client: QQAndroidClient, + contact: Contact, + thumbnailFileMd5: ByteArray, + thumbnailFileSize: Long, + videoFileName: String, + videoFileMd5: ByteArray, + videoFileSize: Long, + videoFileFormat: String + ) = buildOutgoingUniPacket(client) { sequenceId -> + writeProtoBuf( + PttShortVideo.ReqBody.serializer(), + PttShortVideo.ReqBody( + cmd = 300, + seq = sequenceId, + msgPttShortVideoUploadReq = buildShortVideoFileInfo( + client, + contact, + thumbnailFileMd5, + thumbnailFileSize, + videoFileName, + videoFileMd5, + videoFileSize, + videoFileFormat + ), + msgExtensionReq = listOf( + PttShortVideo.ExtensionReq( + subBusiType = 0, + userCnt = 1 + ) + ) + ) + ) + } + + internal fun buildShortVideoFileInfo( + client: QQAndroidClient, + contact: Contact, + thumbnailFileMd5: ByteArray, + thumbnailFileSize: Long, + videoFileName: String, + videoFileMd5: ByteArray, + videoFileSize: Long, + videoFileFormat: String + ) = PttShortVideo.PttShortVideoUploadReq( + fromuin = client.uin, + touin = contact.uin, + chatType = 1, // guild channel = 4, others = 1 + clientType = 2, + msgPttShortVideoFileInfo = PttShortVideo.PttShortVideoFileInfo( + fileName = videoFileName + videoFileFormat, + fileMd5 = videoFileMd5, + fileSize = videoFileSize, + fileResLength = 1280, + fileResWidth = 720, + // Lcom/tencent/mobileqq/transfile/ShortVideoUploadProcessor;getFormat(Ljava/lang/String;)I + fileFormat = 3, + fileTime = 120, + thumbFileMd5 = thumbnailFileMd5, + thumbFileSize = thumbnailFileSize + ), + groupCode = if (contact is Group) contact.uin else 0, + flagSupportLargeSize = 1 + ) + } + + object ShortVideoDownReq : OutgoingPacketFactory("PttCenterSvr.ShortVideoDownReq") { + sealed class Response : Packet { + class Success(val fileMd5: ByteArray, val urlV4: String, val urlV6: String?) : Response() { + override fun toString(): String { + return "PttCenterSvr.ShortVideoDownReq.Response.Success(" + + "urlV4=$urlV4, urlV6=$urlV6)" + } + } + + object Failed : Response() { + override fun toString(): String { + return "PttCenterSvr.ShortVideoDownReq.Response.Failed" + } + } + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { + val resp = readProtoBuf(PttShortVideo.RspBody.serializer()) + + val shortVideoDownloadResp = resp.msgPttShortVideoDownloadResp ?: return Response.Failed + val attr = shortVideoDownloadResp.msgDownloadAddr ?: return Response.Failed + + val fileMd5 = shortVideoDownloadResp.fileMd5 + val urlV4 = attr.strHost.first() + attr.urlArgs + val urlV6 = attr.strHostIpv6.firstOrNull()?.plus(attr.urlArgs) + + return Response.Success(fileMd5, urlV4, urlV6) + } + + // Lcom/tencent/mobileqq/transfile/protohandler/ShortVideoDownHandler;constructReqBody(Ljava/util/List;)[B + operator fun invoke( + client: QQAndroidClient, + contact: Contact, + sender: User, + videoFIleId: String, + videoFileMd5: ByteArray, + ) = buildOutgoingUniPacket(client) { sequenceId -> + writeProtoBuf( + PttShortVideo.ReqBody.serializer(), + PttShortVideo.ReqBody( + cmd = 400, + seq = sequenceId, + msgPttShortVideoDownloadReq = PttShortVideo.PttShortVideoDownloadReq( + fromuin = sender.uin, + touin = client.uin, + chatType = if (sender is Friend) 0 else 1, + clientType = 7, + fileid = videoFIleId, + groupCode = if (contact is Group) contact.uin else 0L, + fileMd5 = videoFileMd5, + businessType = 1, + flagSupportLargeSize = 1, + flagClientQuicProtoEnable = 1, + fileType = 2, // maybe 1 = newly uploaded video, unverified + downType = 2, + sceneType = 2, // hooked 0 and 1, but unknown + reqTransferType = 1, + reqHostType = 11, + ), + msgExtensionReq = listOf( + PttShortVideo.ExtensionReq(subBusiType = 0) + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/ExternalResourceImpl.kt b/mirai-core/src/commonMain/kotlin/utils/ExternalResourceImpl.kt new file mode 100644 index 0000000000..046ed22007 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/ExternalResourceImpl.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2019-2023 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.utils + +import net.mamoe.mirai.utils.ExternalResource + +@Suppress("FunctionName") +internal expect fun CombinedExternalResource(vararg resources: ExternalResource): ExternalResource \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt b/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt index d83a597ee1..352b39bdc4 100644 --- a/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt +++ b/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt @@ -81,6 +81,10 @@ internal object MiraiCoreServices { msgProtocol, "net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol" ) { net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol() } + Services.register( + msgProtocol, + "net.mamoe.mirai.internal.message.protocol.impl.ShortVideoProtocol" + ) { net.mamoe.mirai.internal.message.protocol.impl.ShortVideoProtocol() } Services.register( msgProtocol, "net.mamoe.mirai.internal.message.protocol.impl.TextProtocol" diff --git a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol index 8cabda9dc5..ef877583ef 100644 --- a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol +++ b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol @@ -20,6 +20,7 @@ net.mamoe.mirai.internal.message.protocol.impl.PokeMessageProtocol net.mamoe.mirai.internal.message.protocol.impl.PttMessageProtocol net.mamoe.mirai.internal.message.protocol.impl.QuoteReplyProtocol net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol +net.mamoe.mirai.internal.message.protocol.impl.ShortVideoProtocol net.mamoe.mirai.internal.message.protocol.impl.TextProtocol net.mamoe.mirai.internal.message.protocol.impl.VipFaceProtocol net.mamoe.mirai.internal.message.protocol.impl.ForwardMessageProtocol diff --git a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.InternalShortVideoProtocol b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.InternalShortVideoProtocol new file mode 100644 index 0000000000..82ea60f231 --- /dev/null +++ b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.InternalShortVideoProtocol @@ -0,0 +1,10 @@ +# +# Copyright 2019-2022 Mamoe Technologies and contributors. +# +# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. +# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. +# +# https://github.com/mamoe/mirai/blob/dev/LICENSE +# + +net.mamoe.mirai.internal.message.image.InternalShortVideoProtocolImpl \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/RefineContextTest.kt b/mirai-core/src/commonTest/kotlin/message/RefineContextTest.kt new file mode 100644 index 0000000000..28cca2ff5e --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/RefineContextTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2023 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.test.AbstractTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class RefineContextTest : AbstractTest() { + @Test + fun `merge test`() { + val Key1 = RefineContextKey("KeyInt") + val Key2 = RefineContextKey("KeyDouble") + val Key3 = RefineContextKey("KeyString") + val Key4 = RefineContextKey("KeyBytes") + + val context1 = SimpleRefineContext( + Key1 to 114514, + Key2 to 1919.810, + Key3 to "sodayo" + ) + + val context2 = SimpleRefineContext( + Key2 to 1919.811, + Key3 to "yarimasune", + Key4 to byteArrayOf(11, 45, 14) + ) + + val combinedOverride = context1.merge(context2, override = true) + val combinedNotOverride = context1.merge(context2, override = false) + + val context3 = SimpleRefineContext( + Key2 to 1919.811, + Key3 to "yarimasune" + ) + + assertEquals(context1, context1.merge(context3, false)) + assertTrue(combinedOverride != combinedNotOverride) + + assertEquals(4, combinedOverride.entries().size) + assertEquals(1919.811, combinedOverride[Key2]) + assertEquals(1919.810, combinedNotOverride[Key2]) + assertEquals("sodayo", combinedNotOverride[Key3]) + assertTrue(byteArrayOf(11, 45, 14).contentEquals(combinedNotOverride[Key4])) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/data/MessageRefineTest.kt b/mirai-core/src/commonTest/kotlin/message/data/MessageRefineTest.kt index 72106cdc7e..f735a03ce1 100644 --- a/mirai-core/src/commonTest/kotlin/message/data/MessageRefineTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/data/MessageRefineTest.kt @@ -293,7 +293,10 @@ internal class MessageRefineTest : AbstractTestWithMiraiImpl() { 1234567890, 1617378549, "群垃圾,时不时来被gc", PlainText("5") ), ForwardMessage.Node( - 1234567890, 1617382639, "群垃圾,时不时来被gc", redefined[2].messageChain[QuoteReply]!! + PlainText("aseff") + 1234567890, + 1617382639, + "群垃圾,时不时来被gc", + redefined[2].messageChain[QuoteReply]!! + PlainText("aseff") ), ), redefined, @@ -370,10 +373,12 @@ private fun assertMessageChainEquals(expected: MessageChain, actual: MessageChai if (a !is QuoteReply) return false if (!compare(e.source.originalMessage, a.source.originalMessage)) return false } + is MessageSource -> { if (a !is MessageSource) return false if (!compare(e.originalMessage, a.originalMessage)) return false } + is ForwardMessage -> { if (a !is ForwardMessage) return false if (e.brief != a.brief) return false @@ -383,10 +388,12 @@ private fun assertMessageChainEquals(expected: MessageChain, actual: MessageChai if (e.preview != a.preview) return false assertNodesEquals(e.nodeList, a.nodeList) } + is Image -> { if (a !is Image) return false if (e.imageId != a.imageId) return false } + else -> { if (e != a) return false } diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt index 004ffbc2ce..48efd0aad9 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/MessageProtocolFacadeTest.kt @@ -32,6 +32,7 @@ internal class MessageProtocolFacadeTest : AbstractTest() { PokeMessageProtocol PttMessageProtocol RichMessageProtocol + ShortVideoProtocol TextProtocol VipFaceProtocol ForwardMessageProtocol diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt index 1d2c10ef8b..5c2733ebde 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt @@ -236,7 +236,12 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler protected open fun Deferred.doDecoderChecks() { val config = this.getCompleted() doDecoderChecks(config.messageChain, protocols) { - decodeAndRefineLight(config.elems, config.groupIdOrZero, config.messageSourceKind, bot) + decodeAndRefineLight( + config.elems, + config.groupIdOrZero, + config.messageSourceKind, + bot + ) } } @@ -280,6 +285,7 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler sender = bot, target = defaultTarget ) + is Friend -> OnlineMessageSourceToFriendImpl( sequenceIds = intArrayOf(1), internalIds = intArrayOf(1), @@ -288,6 +294,7 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler sender = bot, target = defaultTarget ) + else -> error("Unexpected target: $defaultTarget") } } diff --git a/mirai-core/src/jvmBaseMain/kotlin/utils/ExternalResourceImpl.kt b/mirai-core/src/jvmBaseMain/kotlin/utils/ExternalResourceImpl.kt new file mode 100644 index 0000000000..505689456b --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/utils/ExternalResourceImpl.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2023 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.utils + +import io.ktor.utils.io.core.* +import io.ktor.utils.io.streams.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.md5 +import net.mamoe.mirai.utils.sha1 +import java.io.InputStream +import java.io.SequenceInputStream +import java.util.Collections + +@Suppress("FunctionName") +internal actual fun CombinedExternalResource(vararg resources: ExternalResource): ExternalResource { + return CombinedExternalResource(resources.toList()) +} + +/** + * it is caller's responsibility to guarantee the immutability of the stream. + */ +internal class CombinedExternalResource( + private val inputs: Collection +) : ExternalResource { + override val isAutoClose: Boolean = true + + override val size: Long = inputs.sumOf { it.size } + override val md5: ByteArray by lazy { combine().md5() } + override val sha1: ByteArray by lazy { combine().sha1() } + + override val formatName: String = "" + + private val _closed = CompletableDeferred() + override val closed: Deferred + get() = _closed + + override fun close() { + _closed.complete(Unit) + } + + override fun inputStream(): InputStream = combine() + + @MiraiInternalApi + override fun input(): Input = inputStream().asInput() + + private fun combine(): InputStream { + return SequenceInputStream(Collections.enumeration(inputs.map { it.inputStream() })) + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseTest/kotlin/utils/CombinedExternalResourceTest.kt b/mirai-core/src/jvmBaseTest/kotlin/utils/CombinedExternalResourceTest.kt new file mode 100644 index 0000000000..129a0c48c7 --- /dev/null +++ b/mirai-core/src/jvmBaseTest/kotlin/utils/CombinedExternalResourceTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2023 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.utils + +import io.ktor.utils.io.core.* +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.text.toByteArray + +class CombinedExternalResourceTest : AbstractTest() { + @Test + fun `work`() { + val res1 = STRING_1.toByteArray().toExternalResource() + val res2 = STRING_2.toByteArray().toExternalResource() + + val combined1 = buildPacket { + res1.input().use { it.copyTo(this) } + res2.input().use { it.copyTo(this) } + }.readBytes().toExternalResource() + + val combined2 = CombinedExternalResource(res1, res2) + + assertEquals(combined1.size, combined2.size) + assertTrue { combined1.md5.contentEquals(combined2.md5) } + assertTrue { combined1.sha1.contentEquals(combined2.sha1) } + } + + + private val STRING_1 = """ + b4FNDvv49gMInP29t82fPJuWQ4ArG1k1YVeCN3UReWXplm4H2S4Rp7zTpt8WXRQEtTL7VemlTIytPbwUkus7qgPVsyUCFreRR1vB3QhRznXqcT06fDkXJQJKyyBGEdwddNWZAkqZcdrOk679sG14kKK5GexaQUmdfTivT5VPO8w1yoWPcUHPfpjB0shCEzjkHI84LJbWNRCVjoZhy0jZAKZxLrsi1sGhl30QcXCFnHpPhWbED8Er9c8gVbjYsG8ejaUlbeNNdKW3GoOpgjFLbwZoQI4QZZgvP5jhBWUPiMG3MCcPlYRSgTf70JpDVTE0YOLhXdJJxz87S8MR4M7rU0WO7ZRkoFOQpFHdmfMmJxbiATHHkOyHVhu1mvA0L72MNtDQP5GcKlDbDcdJL7om4FmekAVVnh7R + """.trimIndent() + private val STRING_2 = """ + FdDoAZt2hJkKAfEWBNWO44R0tJRmApqIwHDD05oW0jyLVVPOdcPaFjY1muYM1qa6jbhZppWYm1oOmgbpFgdPZRYDgzznR0kSapdqXeSSevV4ww4E1U71ELDMsq4f0a1Y8K6UxIOpQl1n20eoe80fHuXKkfN6kbhROBXcwGbiFRpPg5k8G5hCerQQunQyNoeEZrbKacq2OYkOEJV57LuSbBTF4FMZYxCEp1a8omnK1EUHC1Go5pGy0dovz78KpCshPr7MHNMnRu0FiuJ1WYT8ri8iXWsTx3AMxHRjCYfJgrtqc86L3HW0V6Wr8FqFMJLtFl4PgXj5etfRSaaqRJFIZ3nWiRqW48JMRqdGRvLTUWs1Zoa8H11bych18MVypUQJOyxghLLJw0ZP4CvSNUeJOEMitxFxyzjC + """.trimIndent() +} \ No newline at end of file