1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-24 20:43:33 +08:00

initial support for ShortVideo message

This commit is contained in:
StageGuard 2023-07-17 12:20:48 +08:00
parent bd3f50f848
commit 5a949d1f2f
No known key found for this signature in database
GPG Key ID: F6FF8760A883492B
21 changed files with 720 additions and 20 deletions

View File

@ -0,0 +1,49 @@
/*
* 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 net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.safeCast
public interface ShortVideo : MessageContent, ConstrainSingle {
public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, ShortVideo>(MessageContent, { it.safeCast() })
/**
* 文件 ID.
*/
public val fileId: String
/**
* 文件 MD5. 16 bytes.
*/
public val fileMd5: ByteArray
@MiraiInternalApi
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitShortVideo(this, data)
}
override val key: MessageKey<*>
get() = Key
}
@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 = "OnlineShortAudio"
}
}

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

@ -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.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal
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,20 @@ 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(
mutableMapOf(
OnlineShortVideoMsgInternal.MessageSourceKind to MessageSourceKind.GROUP,
OnlineShortVideoMsgInternal.FromId to it.msgHead.fromUin,
OnlineShortVideoMsgInternal.GroupIdOrZero to contact.uin,
)
)
)
}
)
currentSeq = resp.msgElem.first().msgHead.msgSeq

View File

@ -16,9 +16,12 @@ 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.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal
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
import net.mamoe.mirai.utils.cast
internal sealed class TimeBasedRoamingMessagesImpl : AbstractRoamingMessages() {
override suspend fun getMessagesIn(
@ -32,13 +35,34 @@ 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 {
emit(
it.toMessageChainOnline(
contact.bot,
refineContext = SimpleRefineContext(
mutableListOf(
OnlineShortVideoMsgInternal.FromId to it.msgHead.fromUin
).cast()
)
)
)
}
} else {
for (message in messages) {
if (filter.invoke(createRoamingMessage(message, messages))) {
emit(message.toMessageChainOnline(contact.bot))
emit(
message.toMessageChainOnline(
contact.bot,
refineContext = SimpleRefineContext(
mutableListOf(
OnlineShortVideoMsgInternal.FromId to message.msgHead.fromUin
).cast()
)
)
)
}
}
}

View File

@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbish
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio
import net.mamoe.mirai.internal.message.data.LongMessageInternal
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal
import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade
import net.mamoe.mirai.internal.message.protocol.impl.PokeMessageProtocol.Companion.UNSUPPORTED_POKE_MESSAGE_PLAIN
import net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol.Companion.UNSUPPORTED_MERGED_MESSAGE_PLAIN
@ -24,6 +25,7 @@ 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
@ -34,12 +36,13 @@ internal fun ImMsgBody.SourceMsg.toMessageChainNoSource(
bot: Bot,
messageSourceKind: MessageSourceKind,
groupIdOrZero: Long,
fromId: Long,
refineContext: RefineContext = EmptyRefineContext,
facade: MessageProtocolFacade = MessageProtocolFacade
): MessageChain {
val elements = this.elems
return buildMessageChain(elements.size + 1) {
facade.decode(elements, groupIdOrZero, messageSourceKind, bot, this, null)
facade.decode(elements, groupIdOrZero, messageSourceKind, fromId, bot, this, null)
}.cleanupRubbishMessageElements().refineLight(bot, refineContext)
}
@ -78,7 +81,21 @@ internal suspend fun MsgComm.Msg.toMessageChainOnline(
MessageSourceKind.GROUP -> msgHead.groupInfo?.groupCode ?: 0
else -> 0
}
return listOf(this).toMessageChainOnline(bot, groupId, kind, refineContext, facade)
val mutableRefineContextApplier: MutableRefineContext.() -> Unit = {
set(OnlineShortVideoMsgInternal.MessageSourceKind, kind)
set(OnlineShortVideoMsgInternal.GroupIdOrZero, groupId)
}
return listOf(this).toMessageChainOnline(
bot,
groupId,
kind,
// TODO: it is better to add `RefineContext.merge(other, override)` to merge with another refine context
(refineContext.castOrNull<MutableRefineContext>() ?: SimpleRefineContext(mutableMapOf()))
.apply(mutableRefineContextApplier),
facade
)
}
//internal fun List<MsgComm.Msg>.toMessageChainOffline(
@ -129,13 +146,21 @@ 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)
messageList.forEach { msg ->
facade.decode(msg.msgBody.richText.elems, groupIdOrZero, messageSourceKind, bot, builder, msg)
facade.decode(
msg.msgBody.richText.elems,
groupIdOrZero,
messageSourceKind,
source?.fromId ?: first().msgHead.fromUin,
bot,
builder,
msg
)
}
for (msg in messageList) {

View File

@ -0,0 +1,142 @@
/*
* 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.User
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.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.packet.chat.video.PttCenterSvr
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSourceKind
import net.mamoe.mirai.message.data.OnlineShortVideo
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.isSameType
import net.mamoe.mirai.utils.toUHexString
/**
* 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[MessageSourceKind] ?: return null
val fromId = refineContext[FromId] ?: return null
val groupId = refineContext[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 // don't process stranger's video message
}.cast<Contact>()
val sender = when (sourceKind) {
net.mamoe.mirai.message.data.MessageSourceKind.FRIEND -> bot.getFriend(fromId)
net.mamoe.mirai.message.data.MessageSourceKind.GROUP -> {
val group = bot.getGroup(groupId)
checkNotNull(group).members[fromId]
}
else -> return null // don't process stranger's video message
}.cast<User>()
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("")}"
)
return OnlineShortVideoImpl(
videoFile.fileUuid.decodeToString(),
shortVideoDownloadReq.fileMd5,
shortVideoDownloadReq.urlV4
)
}
override fun toString(): String {
TODO("Not yet implemented")
}
override fun contentToString(): String {
TODO("Not yet implemented")
}
companion object {
val MessageSourceKind = RefineContextKey<MessageSourceKind>("MessageSourceKind")
val FromId = RefineContextKey<Long>("FromId")
val GroupIdOrZero = RefineContextKey<Long>("GroupIdOrZero")
}
}
@Suppress("DuplicatedCode")
@SerialName(OnlineShortVideo.SERIAL_NAME)
@Serializable
internal class OnlineShortVideoImpl(
override val fileId: String,
override val fileMd5: ByteArray,
override val urlForDownload: String
) : OnlineShortVideo {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (!isSameType(this, other)) return false
if (fileId != other.fileId) return false
if (urlForDownload != other.urlForDownload) return false
if (!fileMd5.contentEquals(other.fileMd5)) return false
return true
}
override fun toString(): String {
return "[mirai:svideo:$fileId, md5=${fileMd5.toUHexString("")}]"
}
override fun contentToString(): String {
return "[视频]"
}
override fun hashCode(): Int {
var result = fileId.hashCode()
result = 31 * result + fileMd5.contentHashCode()
result = 31 * result + urlForDownload.hashCode()
return result
}
}

View File

@ -77,6 +77,7 @@ internal interface MessageProtocolFacade {
elements: List<ImMsgBody.Elem>,
groupIdOrZero: Long,
messageSourceKind: MessageSourceKind,
fromId: Long,
bot: Bot,
builder: MessageChainBuilder,
containingMsg: MsgComm.Msg? = null,
@ -134,8 +135,11 @@ internal interface MessageProtocolFacade {
elements: List<ImMsgBody.Elem>,
groupIdOrZero: Long,
messageSourceKind: MessageSourceKind,
fromId: Long,
bot: Bot,
): MessageChain = buildMessageChain { decode(elements, groupIdOrZero, messageSourceKind, bot, this, null) }
): MessageChain = buildMessageChain {
decode(elements, groupIdOrZero, messageSourceKind, fromId, bot, this, null)
}
fun createSerializersModule(): SerializersModule = SerializersModule {
@ -171,17 +175,19 @@ internal fun MessageProtocolFacade.decodeAndRefineLight(
elements: List<ImMsgBody.Elem>,
groupIdOrZero: Long,
messageSourceKind: MessageSourceKind,
fromId: Long,
bot: Bot,
refineContext: RefineContext = EmptyRefineContext
): MessageChain = decode(elements, groupIdOrZero, messageSourceKind, bot).refineLight(bot, refineContext)
): MessageChain = decode(elements, groupIdOrZero, messageSourceKind, fromId, bot).refineLight(bot, refineContext)
internal suspend fun MessageProtocolFacade.decodeAndRefineDeep(
elements: List<ImMsgBody.Elem>,
groupIdOrZero: Long,
messageSourceKind: MessageSourceKind,
fromId: Long,
bot: Bot,
refineContext: RefineContext = EmptyRefineContext
): MessageChain = decode(elements, groupIdOrZero, messageSourceKind, bot).refineDeep(bot, refineContext)
): MessageChain = decode(elements, groupIdOrZero, messageSourceKind, fromId, bot).refineDeep(bot, refineContext)
private const val errorTips =
@ -288,6 +294,7 @@ internal class MessageProtocolFacadeImpl(
elements: List<ImMsgBody.Elem>,
groupIdOrZero: Long,
messageSourceKind: MessageSourceKind,
fromId: Long,
bot: Bot,
builder: MessageChainBuilder,
containingMsg: MsgComm.Msg?
@ -336,6 +343,7 @@ internal class MessageProtocolFacadeImpl(
return getSingleReceipt(result, message)
}
override suspend fun <C : AbstractContact> preprocessAndSendOutgoing(
target: C,
message: Message,
@ -378,6 +386,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,32 @@
/*
* 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.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.network.protocol.data.proto.ImMsgBody
internal class ShortVideoProtocol : MessageProtocol() {
override fun ProcessorCollector.collectProcessorsImpl() {
add(Decoder())
}
private class Decoder : MessageDecoder {
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
val videoFile = data.videoFile ?: return
markAsConsumed()
collect(OnlineShortVideoMsgInternal(videoFile))
}
}
}

View File

@ -183,7 +183,9 @@ 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 {
delegate.toMessageChainNoSource(bot, messageSourceKind, groupIdOrZero, delegate.senderUin)
},
fromId = delegate.senderUin,
targetId = when {
groupIdOrZero != 0L -> groupIdOrZero
@ -191,6 +193,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

@ -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.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal
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(
OnlineShortVideoMsgInternal.MessageSourceKind to MessageSourceKind.GROUP,
OnlineShortVideoMsgInternal.FromId to sender.uin,
OnlineShortVideoMsgInternal.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(
OnlineShortVideoMsgInternal.MessageSourceKind to MessageSourceKind.GROUP,
OnlineShortVideoMsgInternal.FromId to sender.uin,
OnlineShortVideoMsgInternal.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.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.OnlineShortVideoMsgInternal
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,18 @@ 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(
mutableMapOf(
OnlineShortVideoMsgInternal.MessageSourceKind to MessageSourceKind.FRIEND,
OnlineShortVideoMsgInternal.FromId to user.uin,
OnlineShortVideoMsgInternal.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,7 @@ internal object KnownPacketFactories {
PttStore.GroupPttUp,
PttStore.GroupPttDown,
PttStore.C2CPttDown,
PttCenterSvr.ShortVideoDownReq,
LongConn.OffPicUp,
// LongConn.OffPicDown,
TroopManagement.EditSpecialTitle,

View File

@ -0,0 +1,94 @@
/*
* 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 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

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

@ -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,
@ -313,7 +316,7 @@ private fun sourceStub(
private suspend fun testRecursiveRefine(list: List<ImMsgBody.Elem>, expected: MessageChain, isLight: Boolean) {
val actual = buildMessageChain {
MessageProtocolFacade.decode(list, 0, MessageSourceKind.GROUP, bot, this, null)
MessageProtocolFacade.decode(list, 0, MessageSourceKind.GROUP, 0L, bot, this, null)
}.let { c ->
if (isLight) {
c.refineLight(bot)
@ -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
ShortVideoMessageProtocol
TextProtocol
VipFaceProtocol
ForwardMessageProtocol

View File

@ -236,7 +236,13 @@ 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,
config.target?.id ?: 0L,
bot
)
}
}
@ -280,6 +286,7 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler
sender = bot,
target = defaultTarget
)
is Friend -> OnlineMessageSourceToFriendImpl(
sequenceIds = intArrayOf(1),
internalIds = intArrayOf(1),
@ -288,6 +295,7 @@ internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandler
sender = bot,
target = defaultTarget
)
else -> error("Unexpected target: $defaultTarget")
}
}

View File

@ -57,6 +57,7 @@ internal class FaceProtocolTest : AbstractMessageProtocolTest() {
),
groupIdOrZero = 0,
MessageSourceKind.GROUP,
fromId = 0L,
bot,
)
}