Redesign MultiMsg; Support nested ForwardMessage sending; close #1198

This commit is contained in:
Karlatemp 2021-11-11 23:34:21 +08:00
parent b42a57736d
commit b5d8c708bd
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
6 changed files with 284 additions and 80 deletions

View File

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

View File

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

View File

@ -297,7 +297,7 @@ internal suspend fun <C : Contact> SendMessageHandler<C>.transformSpecialMessage
)
return RichMessage.forwardMessage(
resId = resId,
timeSeconds = currentTimeSeconds(),
fileName = currentTimeSeconds().toString(),
forwardMessage = forward,
)
}

View File

@ -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("&", "&amp;")
}
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>

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

View File

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