feature: add ShortVideo message support (#2739)

* 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
This commit is contained in:
StageGuard 2023-08-20 18:41:05 +08:00 committed by GitHub
parent c4815c9a7f
commit 5b3e508b75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1893 additions and 39 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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] 到临时文件并将其作为图片发送到指定联系人

View File

@ -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')"
}
}
}

View File

@ -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"

View File

@ -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 <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitShortVideo(this, data)
}
override val key: MessageKey<*>
get() = Key
public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, ShortVideo>(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, OnlineShortVideo>(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, OfflineShortVideo>(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"
)
}
}
}

View File

@ -41,6 +41,8 @@ public interface MessageVisitor<in D, out R> {
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<in D, out R> : MessageVisitor<D, R>
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)
}

View File

@ -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

View File

@ -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"
}

View File

@ -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<String, String> = mutableMapOf(
"FFD8FF" to "jpg",
"89504E47" to "png",
"47494638" to "gif",
private val FILE_TYPES: List<FileType> = 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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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))
}
}
}

View File

@ -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<MsgComm.Msg>.toMessageChainOffline(
@ -129,13 +141,28 @@ private fun List<MsgComm.Msg>.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) {

View File

@ -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<T : Any>(
append(')')
}
}
internal companion object {
val MessageSourceKind = RefineContextKey<MessageSourceKind>("MessageSourceKind")
val FromId = RefineContextKey<Long>("FromId")
val GroupIdOrZero = RefineContextKey<Long>("GroupIdOrZero")
}
}
/**
@ -108,6 +115,8 @@ internal interface RefineContext {
operator fun contains(key: RefineContextKey<*>): Boolean
operator fun <T : Any> get(key: RefineContextKey<T>): T?
fun <T : Any> getNotNull(key: RefineContextKey<T>): T = get(key) ?: error("No such value of `$key`")
fun merge(other: RefineContext, override: Boolean): RefineContext
fun entries(): Set<Pair<RefineContextKey<*>, 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 <T : Any> get(key: RefineContextKey<T>): T? = null
override fun merge(other: RefineContext, override: Boolean): RefineContext {
return other
}
override fun entries(): Set<Pair<RefineContextKey<*>, 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<Pair<RefineContextKey<*>, 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<Any>] = 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<RefineContextKey<*>, Any>): SimpleRefineContext =
SimpleRefineContext(elements.toMap().toMutableMap())
/**
* 执行不需要 `suspend` refine. 用于 [MessageSource.originalMessage].
*/

View File

@ -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
}
}

View File

@ -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
)
)
}
}

View File

@ -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 <C : AbstractContact> 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",

View File

@ -29,6 +29,7 @@ internal interface MessageDecoderContext : ProcessorPipelineContext<ImMsgBody.El
val MESSAGE_SOURCE_KIND = TypeKey<MessageSourceKind>("messageSourceKind")
val GROUP_ID = TypeKey<Long>("groupId") // zero if not group
val CONTAINING_MSG = TypeKey<MsgComm.Msg?>("containingMsg")
val FROM_ID = TypeKey<Long>("fromId") // group/temp = sender, friend/stranger = this
}
}

View File

@ -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<AbstractShortVideoWithThumbnail> {
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<Pair<String, Int>> = 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
)
}
}

View File

@ -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(

View File

@ -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

View File

@ -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,
),

View File

@ -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<MsgComm.Msg>(type
val group = bot.getGroupByUinOrCode(tmpHead.groupUin) ?: return
handlePrivateMessage(data, group[senderUin] ?: return)
}
else -> markNotConsumed()
}
@ -129,7 +133,16 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor<MsgComm.Msg>(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) {

View File

@ -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<String> = emptyList(),
@JvmField @ProtoNumber(11) val urlArgs: String = "",
@JvmField @ProtoNumber(21) val strHostIpv6: List<String> = emptyList(),
@JvmField @ProtoNumber(22) val strDomain: List<String> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(4) val diffAreaOutAddr: List<PttShortVideoIpList> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(8) val diffAreaInnerAddr: List<PttShortVideoIpList> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(4) val diffAreaOutAddr: List<PttShortVideoIpList> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(9) val diffAreaInnerAddr: List<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(10) val dataHole: List<DataHole> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(4) val diffAreaOutAddr: List<PttShortVideoIpList> = 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<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(9) val diffAreaInnerAddr: List<PttShortVideoIpList> = emptyList(),
@JvmField @ProtoNumber(10) val dataHole: List<DataHole> = 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<PttShortVideoRetweetReq> = emptyList(),
@JvmField @ProtoNumber(6) val msgShortVideoDeleteReq: List<PttShortVideoDeleteReq> = emptyList(),
@JvmField @ProtoNumber(100) val msgExtensionReq: List<ExtensionReq> = 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<PttShortVideoRetweetResp> = emptyList(),
@JvmField @ProtoNumber(6) val msgShortVideoDeleteResp: List<PttShortVideoDeleteResp> = emptyList(),
@JvmField @ProtoNumber(100) val changeChannel: Int = 0,
@JvmField @ProtoNumber(101) val allowRetry: Int = 0
) : ProtoBuf
}

View File

@ -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,

View File

@ -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<GroupShortVideoUpReq.Response>("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<ShortVideoDownReq.Response>("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)
)
)
)
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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<Int>("KeyInt")
val Key2 = RefineContextKey<Double>("KeyDouble")
val Key3 = RefineContextKey<String>("KeyString")
val Key4 = RefineContextKey<ByteArray>("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]))
}
}

View File

@ -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
}

View File

@ -32,6 +32,7 @@ internal class MessageProtocolFacadeTest : AbstractTest() {
PokeMessageProtocol
PttMessageProtocol
RichMessageProtocol
ShortVideoProtocol
TextProtocol
VipFaceProtocol
ForwardMessageProtocol

View File

@ -236,7 +236,12 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler
protected open fun Deferred<ChecksConfiguration>.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")
}
}

View File

@ -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>
) : 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<Unit>()
override val closed: Deferred<Unit>
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() }))
}
}

View File

@ -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()
}