From b5d8c708bd5a732f57e91c9aad4bd8e04f25a23d Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Thu, 11 Nov 2021 23:34:21 +0800 Subject: [PATCH] Redesign MultiMsg; Support nested ForwardMessage sending; close #1198 --- .../src/commonMain/kotlin/Numbers.kt | 1 + mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 83 ++---- .../kotlin/contact/SendMessageHandler.kt | 2 +- .../kotlin/message/LongMessageInternal.kt | 23 +- .../kotlin/message/MultiMsgUploader.kt | 247 ++++++++++++++++++ .../kotlin/message/ReceiveMessageHandler.kt | 8 +- 6 files changed, 284 insertions(+), 80 deletions(-) create mode 100644 mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt diff --git a/mirai-core-utils/src/commonMain/kotlin/Numbers.kt b/mirai-core-utils/src/commonMain/kotlin/Numbers.kt index 3165105ac..f85a6d4a2 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Numbers.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Numbers.kt @@ -15,3 +15,4 @@ package net.mamoe.mirai.utils public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF) public fun Short.toIntUnsigned(): Int = this.toUShort().toInt() public fun Byte.toIntUnsigned(): Int = toInt() and 0xFF +public fun Int.concatAsLong(i2: Int): Long = this.toLongUnsigned().shl(Int.SIZE_BITS) or i2.toLongUnsigned() diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 3b23bf519..be4ecafcf 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -15,7 +15,6 @@ import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.util.* -import io.ktor.utils.io.core.* import kotlinx.coroutines.currentCoroutineContext import kotlinx.io.core.discardExact import kotlinx.io.core.readBytes @@ -38,13 +37,19 @@ import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep import net.mamoe.mirai.internal.network.components.EventDispatcher import net.mamoe.mirai.internal.network.components.EventDispatcherScopeFlag -import net.mamoe.mirai.internal.network.highway.* +import net.mamoe.mirai.internal.network.highway.ChannelKind +import net.mamoe.mirai.internal.network.highway.ResourceKind +import net.mamoe.mirai.internal.network.highway.tryDownload +import net.mamoe.mirai.internal.network.highway.tryServersDownload import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit -import net.mamoe.mirai.internal.network.protocol.packet.chat.* +import net.mamoe.mirai.internal.network.protocol.packet.chat.MultiMsg +import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact +import net.mamoe.mirai.internal.network.protocol.packet.chat.NudgePacket +import net.mamoe.mirai.internal.network.protocol.packet.chat.PbMessageSvc 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.login.StatSvc @@ -55,7 +60,6 @@ import net.mamoe.mirai.internal.network.sKey import net.mamoe.mirai.internal.utils.MiraiProtocolInternal import net.mamoe.mirai.internal.utils.crypto.TEA import net.mamoe.mirai.internal.utils.io.serialization.loadAs -import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.data.* @@ -63,9 +67,6 @@ import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1 import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2 import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import kotlin.math.absoluteValue -import kotlin.random.Random internal fun getMiraiImpl() = Mirai as MiraiImpl @@ -632,70 +633,18 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { sendMessageHandler: SendMessageHandler<*>, message: Collection, isLong: Boolean, - ): String = with(bot.asQQAndroidBot()) { + ): String { + bot.asQQAndroidBot() message.forEach { it.messageChain.ensureSequenceIdAvailable() } + val uploader = MultiMsgUploader( + client = bot.client, + isLong = isLong, + handler = sendMessageHandler, + ).also { it.emitMain(message) } - - val data = message.calculateValidationData( - client = client, - random = Random.nextInt().absoluteValue, - sendMessageHandler, - isLong, - ) - - val response = network.run { - MultiMsg.ApplyUp.createForGroup( - buType = if (isLong) 1 else 2, - client = bot.client, - messageData = data, - dstUin = sendMessageHandler.targetUin - ).sendAndExpect() - } - - val resId: String - when (response) { - is MultiMsg.ApplyUp.Response.MessageTooLarge -> - error( - "Internal error: message is too large, but this should be handled before sending. " - ) - is MultiMsg.ApplyUp.Response.RequireUpload -> { - resId = response.proto.msgResid - - val body = LongMsg.ReqBody( - subcmd = 1, - platformType = 9, - termType = 5, - msgUpReq = listOf( - LongMsg.MsgUpReq( - msgType = 3, // group - dstUin = sendMessageHandler.targetUin, - msgId = 0, - msgUkey = response.proto.msgUkey, - needCache = 0, - storeType = 2, - msgContent = data.data - ) - ) - ).toByteArray(LongMsg.ReqBody.serializer()) - - body.toExternalResource().use { resource -> - Highway.uploadResourceBdh( - bot = bot, - resource = resource, - kind = when (isLong) { - true -> ResourceKind.LONG_MESSAGE - false -> ResourceKind.FORWARD_MESSAGE - }, - commandId = 27, - initialTicket = response.proto.msgSig - ) - } - } - } - - return resId + return uploader.uploadAndReturnResId() } override suspend fun solveNewFriendRequestEvent( diff --git a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt index 48aa95c1c..80615f233 100644 --- a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt @@ -297,7 +297,7 @@ internal suspend fun SendMessageHandler.transformSpecialMessage ) return RichMessage.forwardMessage( resId = resId, - timeSeconds = currentTimeSeconds(), + fileName = currentTimeSeconds().toString(), forwardMessage = forward, ) } diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 5888dadac..d0f6e794e 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -80,12 +80,14 @@ internal data class ForwardMessageInternal( val preview = titles val source = xmlFoot.findField("name") - if (fileName != null) { // nested - val transmits = refineContext.getNotNull(MsgTransmits)[fileName] - ?: return SimpleServiceMessage(serviceId, content) // Refine failed + val resId = resId?.takeIf { it.isNotEmpty() } + + if (fileName != null) kotlin.run nested@{ // nested + val transmits = refineContext[MsgTransmits]?.get(fileName) + ?: return@nested // Refine failed return MessageOrigin( SimpleServiceMessage(serviceId, content), - null, // Nested don't have resource id + resId, MessageOriginKind.FORWARD, ) + ForwardMessage( preview = preview, @@ -97,6 +99,11 @@ internal data class ForwardMessageInternal( ) } + // No id and no fileName + if (resId == null) { + return SimpleServiceMessage(serviceId, content) + } + return MessageOrigin( SimpleServiceMessage(serviceId, content), resId, @@ -107,7 +114,7 @@ internal data class ForwardMessageInternal( brief = brief, source = source, summary = summary.trim(), - nodeList = Mirai.downloadForwardMessage(bot, resId!!), + nodeList = Mirai.downloadForwardMessage(bot, resId), ) } @@ -157,19 +164,19 @@ internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSecon } -private fun String.xmlEnc():String { +private fun String.xmlEnc(): String { return this.replace("&", "&") } internal fun RichMessage.Key.forwardMessage( resId: String, - timeSeconds: Long, + fileName: String, forwardMessage: ForwardMessage, ): ForwardMessageInternal = with(forwardMessage) { val template = """ ${title.take(50).xmlEnc()} diff --git a/mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt b/mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt new file mode 100644 index 000000000..2799b21ab --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message + +import io.ktor.utils.io.core.* +import net.mamoe.mirai.internal.contact.SendMessageHandler +import net.mamoe.mirai.internal.contact.takeSingleContent +import net.mamoe.mirai.internal.network.QQAndroidClient +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.ImMsgBody +import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit +import net.mamoe.mirai.internal.network.protocol.packet.chat.MessageValidationData +import net.mamoe.mirai.internal.network.protocol.packet.chat.MultiMsg +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource +import net.mamoe.mirai.utils.concatAsLong +import net.mamoe.mirai.utils.gzip +import net.mamoe.mirai.utils.toLongUnsigned +import kotlin.math.absoluteValue +import kotlin.random.Random + +internal open class MultiMsgUploader( + val client: QQAndroidClient, + val isLong: Boolean, + val handler: SendMessageHandler<*>, + val tmpRand: Random = Random.Default, +) { + + protected open fun newUploader(): MultiMsgUploader = MultiMsgUploader( + isLong = isLong, + handler = handler, + client = client, + tmpRand = tmpRand, + ) + + val mainMsg = mutableListOf() + val nestedMsgs = mutableMapOf>() + + init { + nestedMsgs["MultiMsg"] = mainMsg + } + + protected open fun newNid(): String { + var nid: String + do { + nid = "${tmpRand.nextInt().absoluteValue}" + } while (nestedMsgs.containsKey(nid)) + return nid + } + + open suspend fun emitMain( + nodes: Collection, + ) { + emit("MultiMsg", nodes) + } + + open suspend fun convertNestedForwardMessage(nestedForward: ForwardMessage, msgChain: MessageChain): MessageChain { + suspend fun convertByMessageOrigin(origin: MessageOrigin): MessageChain? { + if (origin.kind != MessageOriginKind.FORWARD) return null + val resId = origin.resourceId + if (resId != null) { + val nid = newNid() + emit(nid, nestedForward.nodeList) + return messageChainOf( + RichMessage.forwardMessage( + resId = resId, + fileName = nid, + forwardMessage = nestedForward, + ) + ) + } + return null + } + + suspend fun convertByReUpload(): MessageChain { + // Upload nested and refine to service msg + val nestedMMUploader = newUploader() + nestedMMUploader.emitMain(nestedForward.nodeList) + + val resId = nestedMMUploader.uploadAndReturnResId() + + val mirror = nestedMMUploader.nestedMsgs + mirror.remove("MultiMsg") + nestedMsgs.putAll(mirror) + + val nid = newNid() + nestedMsgs[nid] = nestedMMUploader.mainMsg + return messageChainOf( + RichMessage.forwardMessage( + resId = resId, + fileName = nid, + forwardMessage = nestedForward, + ) + ) + } + + msgChain.firstIsInstanceOrNull()?.let { origin -> + convertByMessageOrigin(origin)?.let { return it } + } + + return convertByReUpload() + } + + open suspend fun emit(id: String, msgs: Collection) { + val nds = mutableListOf().let { tmp -> + nestedMsgs.putIfAbsent(id, tmp) ?: tmp + } + + val existsIds = mutableSetOf() + + msgs.forEach { msg -> + var msgChain = msg.messageChain + msgChain.takeSingleContent()?.let { nestedForward -> + msgChain = convertNestedForwardMessage(nestedForward, msgChain) + } + + var seq: Int = -1 + var uid: Int = -1 + msg.messageChain.sourceOrNull?.let { source -> + source as MessageSourceInternal + + seq = source.sequenceIds.first() + uid = source.internalIds.first() + } + while (true) { + if (seq != -1 && uid != -1) { + if (existsIds.add(seq.concatAsLong(uid))) break + } + seq = tmpRand.nextInt().absoluteValue + uid = tmpRand.nextInt().absoluteValue + } + + val msg0 = MsgComm.Msg( + msgHead = MsgComm.MsgHead( + fromUin = msg.senderId, + toUin = if (isLong) { + handler.targetUserUin ?: 0 + } else 0, + msgSeq = seq, + msgTime = msg.time, + msgUid = 0x01000000000000000L or uid.toLongUnsigned(), + mutiltransHead = MsgComm.MutilTransHead( + status = 0, + msgId = 1, + ), + msgType = 82, // troop, + groupInfo = handler.run { msg.groupInfo }, + isSrcMsg = false, + ), + msgBody = ImMsgBody.MsgBody( + richText = ImMsgBody.RichText( + elems = msgChain.toRichTextElems( + handler.contact, + withGeneralFlags = false, + isForward = true, + ).toMutableList() + ) + ) + ) + nds.add(msg0) + } + } + + open fun toMessageValidationData(): MessageValidationData { + val msgTransmit = MsgTransmit.PbMultiMsgTransmit( + msg = mainMsg, + pbItemList = nestedMsgs.asSequence() + .map { (name, msgList) -> + MsgTransmit.PbMultiMsgItem( + fileName = name, + buffer = MsgTransmit.PbMultiMsgNew(msgList).toByteArray(MsgTransmit.PbMultiMsgNew.serializer()) + ) + } + .toList() + ) + val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) + + return MessageValidationData(bytes.gzip()) + } + + open suspend fun uploadAndReturnResId(): String { + val data = toMessageValidationData() + + val response = client.bot.network.run { + MultiMsg.ApplyUp.createForGroup( + buType = if (isLong) 1 else 2, + client = client, + messageData = data, + dstUin = handler.targetUin + ).sendAndExpect() + } + + val resId: String + when (response) { + is MultiMsg.ApplyUp.Response.MessageTooLarge -> + error( + "Internal error: message is too large, but this should be handled before sending. " + ) + is MultiMsg.ApplyUp.Response.RequireUpload -> { + resId = response.proto.msgResid + + val body = LongMsg.ReqBody( + subcmd = 1, + platformType = 9, + termType = 5, + msgUpReq = listOf( + LongMsg.MsgUpReq( + msgType = 3, // group + dstUin = handler.targetUin, + msgId = 0, + msgUkey = response.proto.msgUkey, + needCache = 0, + storeType = 2, + msgContent = data.data + ) + ) + ).toByteArray(LongMsg.ReqBody.serializer()) + + body.toExternalResource().use { resource -> + Highway.uploadResourceBdh( + bot = client.bot, + resource = resource, + kind = when (isLong) { + true -> ResourceKind.LONG_MESSAGE + false -> ResourceKind.FORWARD_MESSAGE + }, + commandId = 27, + initialTicket = response.proto.msgSig + ) + } + } + } + + return resId + } +} diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index afe5ad91b..68699a7ff 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -525,22 +525,22 @@ internal object ReceiveMessageTransformer { 35 -> { val resId = findStringProperty("m_resid") + val fileName = findStringProperty("m_fileName").takeIf { it.isNotEmpty() } val msg = if (resId.isEmpty()) { // Nested ForwardMessage - val fileName = findStringProperty("m_fileName") - if (fileName.isNotEmpty() && findStringProperty("action") == "viewMultiMsg") { + if (fileName != null && findStringProperty("action") == "viewMultiMsg") { ForwardMessageInternal(content, null, fileName) } else { SimpleServiceMessage(35, content) } } else when (findStringProperty("multiMsgFlag").toIntOrNull()) { 1 -> LongMessageInternal(content, resId) - 0 -> ForwardMessageInternal(content, resId, null) + 0 -> ForwardMessageInternal(content, resId, fileName) else -> { // from PC QQ if (findStringProperty("action") == "viewMultiMsg") { - ForwardMessageInternal(content, resId, null) + ForwardMessageInternal(content, resId, fileName) } else { SimpleServiceMessage(35, content) }