mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-02 04:30:25 +08:00
Redesign MultiMsg; Support nested ForwardMessage sending; close #1198
This commit is contained in:
parent
b42a57736d
commit
b5d8c708bd
@ -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()
|
||||
|
@ -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<ForwardMessage.INode>,
|
||||
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(
|
||||
|
@ -297,7 +297,7 @@ internal suspend fun <C : Contact> SendMessageHandler<C>.transformSpecialMessage
|
||||
)
|
||||
return RichMessage.forwardMessage(
|
||||
resId = resId,
|
||||
timeSeconds = currentTimeSeconds(),
|
||||
fileName = currentTimeSeconds().toString(),
|
||||
forwardMessage = forward,
|
||||
)
|
||||
}
|
||||
|
@ -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 = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<msg serviceID="35" templateID="1" action="viewMultiMsg" brief="${brief.take(30).xmlEnc()}"
|
||||
m_resid="$resId" m_fileName="$timeSeconds"
|
||||
m_resid="$resId" m_fileName="$fileName"
|
||||
tSum="3" sourceMsgId="0" url="" flag="3" adverSign="0" multiMsgFlag="0">
|
||||
<item layout="1" advertiser_id="0" aid="0">
|
||||
<title size="34" maxLines="2" lineSpace="12">${title.take(50).xmlEnc()}</title>
|
||||
|
247
mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt
Normal file
247
mirai-core/src/commonMain/kotlin/message/MultiMsgUploader.kt
Normal file
@ -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<MsgComm.Msg>()
|
||||
val nestedMsgs = mutableMapOf<String, MutableList<MsgComm.Msg>>()
|
||||
|
||||
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<ForwardMessage.INode>,
|
||||
) {
|
||||
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<MessageOrigin>()?.let { origin ->
|
||||
convertByMessageOrigin(origin)?.let { return it }
|
||||
}
|
||||
|
||||
return convertByReUpload()
|
||||
}
|
||||
|
||||
open suspend fun emit(id: String, msgs: Collection<ForwardMessage.INode>) {
|
||||
val nds = mutableListOf<MsgComm.Msg>().let { tmp ->
|
||||
nestedMsgs.putIfAbsent(id, tmp) ?: tmp
|
||||
}
|
||||
|
||||
val existsIds = mutableSetOf<Long>()
|
||||
|
||||
msgs.forEach { msg ->
|
||||
var msgChain = msg.messageChain
|
||||
msgChain.takeSingleContent<ForwardMessage>()?.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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user