From 1786c95e075bcb5b2962a0d5437571fd15c17a7d Mon Sep 17 00:00:00 2001
From: Him188 <Him188@mamoe.net>
Date: Wed, 22 Apr 2020 22:09:53 +0800
Subject: [PATCH] Support merged forward messages!

---
 .../mirai/qqandroid/QQAndroidBot.common.kt    | 65 +++++++++++++++----
 .../mirai/qqandroid/contact/GroupImpl.kt      | 16 +++--
 .../mirai/qqandroid/message/convension.kt     | 14 +++-
 .../network/protocol/packet/chat/MultiMsg.kt  | 12 ++--
 .../message/data/ForwardMessage.kt            | 48 ++++++++++++++
 .../message/data/RichMessage.kt               | 14 ++--
 6 files changed, 137 insertions(+), 32 deletions(-)
 create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/ForwardMessage.kt

diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
index 623cd1097..3391da4c3 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
@@ -550,12 +550,18 @@ internal abstract class QQAndroidBotBase constructor(
     @JvmSynthetic
     @LowLevelAPI
     @MiraiExperimentalAPI
-    internal suspend fun lowLevelSendLongGroupMessage(groupCode: Long, message: MessageChain): MessageReceipt<Group> {
+    internal suspend fun lowLevelSendGroupLongOrForwardMessage(
+        groupCode: Long,
+        message: Collection<MessageChain>,
+        isLong: Boolean
+    ): MessageReceipt<Group> {
+        message.forEach {
+            it.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
+        }
         val group = getGroup(groupCode)
 
         val time = currentTimeSeconds
         val sequenceId = client.atomicNextMessageSequenceId()
-        message.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
 
         network.run {
             val data = message.calculateValidationDataForGroup(
@@ -569,6 +575,7 @@ internal abstract class QQAndroidBotBase constructor(
 
             val response =
                 MultiMsg.ApplyUp.createForGroupLongMessage(
+                    buType = if (isLong) 1 else 2,
                     client = this@QQAndroidBotBase.client,
                     messageData = data,
                     dstUin = Group.calculateGroupUinByGroupCode(groupCode)
@@ -578,10 +585,7 @@ internal abstract class QQAndroidBotBase constructor(
             when (response) {
                 is MultiMsg.ApplyUp.Response.MessageTooLarge ->
                     error(
-                        "Internal error: message is too large, but this should be handled before sending. Message content:" +
-                                message.joinToString {
-                                    "${it::class.simpleName}(l=${it.toString().length})"
-                                }
+                        "Internal error: message is too large, but this should be handled before sending. "
                     )
                 is MultiMsg.ApplyUp.Response.RequireUpload -> {
                     resId = response.proto.msgResid
@@ -639,13 +643,27 @@ internal abstract class QQAndroidBotBase constructor(
                 }
             }
 
-            return group.sendMessage(
-                RichMessage.longMessage(
-                    brief = message.joinToString(limit = 27) { it.contentToString() },
-                    resId = resId,
-                    timeSeconds = time
+            return if (isLong) {
+                group.sendMessage(
+                    RichMessage.longMessage(
+                        brief = message.joinToString(limit = 27) { it.contentToString() },
+                        resId = resId,
+                        timeSeconds = time
+                    )
                 )
-            )
+            } else {
+                group.sendMessage(
+                    RichMessage.forwardMessage(
+                        resId = resId,
+                        timeSeconds = time,
+                        preview = message.take(3).joinToString {
+                            """
+                                <title size="26" color="#777777" maxLines="2" lineSpace="12">${it.joinToString(limit = 10)}</title>
+                            """.trimIndent()
+                        }
+                    )
+                )
+            }
         }
     }
 
@@ -746,3 +764,26 @@ private fun RichMessage.Templates.longMessage(brief: String, resId: String, time
 
     return LongMessage(template, resId)
 }
+
+
+private fun RichMessage.Templates.forwardMessage(
+    resId: String,
+    timeSeconds: Long,
+    preview: String
+): ForwardMessageInternal {
+    val template = """
+        <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+        <msg serviceID="35" templateID="1" action="viewMultiMsg" brief="[聊天记录]"
+             m_resid="$resId" m_fileName="$timeSeconds"
+             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>
+                $preview
+                <hr hidden="false" style="0"/>
+                <summary size="26" color="#777777">查看3条转发消息</summary>
+            </item>
+            <source name="聊天记录" icon="" action="" appid="-1"/>
+        </msg>
+    """.trimIndent()
+    return ForwardMessageInternal(template)
+}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
index faf25a19b..efe60dd35 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION_ERROR")
+@file:Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION_ERROR", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
 @file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
 
 package net.mamoe.mirai.qqandroid.contact
@@ -289,10 +289,18 @@ internal class GroupImpl(
 
     @OptIn(MiraiExperimentalAPI::class)
     private suspend fun sendMessageImpl(message: Message): MessageReceipt<Group> {
+        if (message is MessageChain) {
+            if (message.anyIsInstance<ForwardMessage>()) {
+                return sendMessageImpl(message.singleOrNull() ?: error("ForwardMessage must be standalone"))
+            }
+        }
+        if (message is ForwardMessage) {
+            return bot.lowLevelSendGroupLongOrForwardMessage(this.id, message.messageList, false)
+        }
 
         val msg: MessageChain
 
-        if (message !is LongMessage) {
+        if (message !is LongMessage && message !is ForwardMessageInternal) {
             val event = GroupMessageSendEvent(this, message.asMessageChain()).broadcast()
             if (event.isCancelled) {
                 throw EventCancelledException("cancelled by GroupMessageSendEvent")
@@ -314,7 +322,7 @@ internal class GroupImpl(
             }
 
             if (length > 702 || imageCnt > 2)
-                return bot.lowLevelSendLongGroupMessage(this.id, event.message)
+                return bot.lowLevelSendGroupLongOrForwardMessage(this.id, listOf(event.message), true)
 
             msg = event.message
         } else msg = message.asMessageChain()
@@ -334,7 +342,7 @@ internal class GroupImpl(
                     120 -> throw BotIsBeingMutedException(this@GroupImpl)
                     34 -> {
                         kotlin.runCatching { // allow retry once
-                            return bot.lowLevelSendLongGroupMessage(id, msg)
+                            return bot.lowLevelSendGroupLongOrForwardMessage(id, listOf(msg), true)
                         }.getOrElse {
                             throw IllegalStateException("internal error: send message failed(34)", it)
                         }
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt
index 8fbac2032..9311431f5 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt
@@ -52,6 +52,17 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
         if (it is RichMessage) {
             val content = MiraiPlatformUtils.zip(it.content.toByteArray())
             when (it) {
+                is ForwardMessageInternal -> {
+                    elements.add(
+                        ImMsgBody.Elem(
+                            richMsg = ImMsgBody.RichMsg(
+                                serviceId = it.serviceId, // ok
+                                template1 = byteArrayOf(1) + content
+                            )
+                        )
+                    )
+                    transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
+                }
                 is LongMessage -> {
                     check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
                     elements.add(
@@ -136,6 +147,7 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
                     }
                 }
             }
+            is ForwardMessage,
             is MessageSource, // mirai metadata only
             is RichMessage // already transformed above
             -> {
@@ -324,7 +336,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, bot: B
                         if (resId != null) {
                             list.add(LongMessage(content, resId))
                         } else {
-                            list.add(ForwardMessage(content))
+                            list.add(ForwardMessageInternal(content))
                         }
                     }
 
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
index 2a2699093..6c3aba232 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
@@ -42,7 +42,7 @@ internal class MessageValidationData @OptIn(MiraiInternalAPI::class) constructor
 }
 
 @OptIn(MiraiInternalAPI::class)
-internal fun MessageChain.calculateValidationDataForGroup(
+internal fun Collection<MessageChain>.calculateValidationDataForGroup(
     sequenceId: Int,
     time: Int,
     random: UInt,
@@ -50,10 +50,9 @@ internal fun MessageChain.calculateValidationDataForGroup(
     botId: Long,
     botMemberNameCard: String
 ): MessageValidationData {
-    val richTextElems = this.toRichTextElems(forGroup = true, withGeneralFlags = false)
 
     val msgTransmit = MsgTransmit.PbMultiMsgTransmit(
-        msg = listOf(
+        msg = this.map { chain ->
             MsgComm.Msg(
                 msgHead = MsgComm.MsgHead(
                     fromUin = botId,
@@ -73,11 +72,11 @@ internal fun MessageChain.calculateValidationDataForGroup(
                 ),
                 msgBody = ImMsgBody.MsgBody(
                     richText = ImMsgBody.RichText(
-                        elems = richTextElems.toMutableList()
+                        elems = chain.toRichTextElems(forGroup = true, withGeneralFlags = false).toMutableList()
                     )
                 )
             )
-        )
+        }
     )
 
     val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
@@ -105,6 +104,7 @@ internal class MultiMsg {
 
         // captured from group
         fun createForGroupLongMessage(
+            buType: Int,
             client: QQAndroidClient,
             messageData: MessageValidationData,
             dstUin: Long // group uin
@@ -112,7 +112,7 @@ internal class MultiMsg {
             writeProtoBuf(
                 MultiMsg.ReqBody.serializer(),
                 MultiMsg.ReqBody(
-                    buType = 1,
+                    buType = buType, // 1: long, 2: 合并转发
                     buildVer = "8.2.0.1296",
                     multimsgApplyupReq = listOf(
                         MultiMsg.MultiMsgApplyUpReq(
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/ForwardMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/ForwardMessage.kt
new file mode 100644
index 000000000..4956a9df0
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/ForwardMessage.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 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/master/LICENSE
+ */
+
+@file:Suppress("MemberVisibilityCanBePrivate")
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import net.mamoe.mirai.utils.SinceMirai
+
+
+/**
+ * 合并转发
+ */
+@SinceMirai("0.39.0")
+class ForwardMessage(
+    val messageList: Collection<MessageChain>
+) : MessageContent {
+    companion object Key : Message.Key<ForwardMessage> {
+        override val typeName: String get() = "ForwardMessage"
+    }
+
+    override fun toString(): String = "[mirai:forward:$messageList]"
+
+
+    private val contentToString: String by lazy {
+        messageList.joinToString("\n")
+    }
+
+    @MiraiExperimentalAPI
+    override fun contentToString(): String = contentToString
+
+    override val length: Int
+        get() = contentToString.length
+
+    override fun get(index: Int): Char = contentToString[length]
+
+    override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
+        contentToString.subSequence(startIndex, endIndex)
+
+    override fun compareTo(other: String): Int = contentToString.compareTo(other)
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
index 629a83587..6a34f1e73 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
@@ -155,22 +155,18 @@ constructor(serviceId: Int = 60, content: String) : ServiceMessage(serviceId, co
 @SinceMirai("0.31.0")
 @MiraiExperimentalAPI
 class LongMessage internal constructor(content: String, val resId: String) : ServiceMessage(35, content) {
-    companion object Key : Message.Key<XmlMessage> {
+    companion object Key : Message.Key<LongMessage> {
         override val typeName: String get() = "LongMessage"
     }
 }
 
 /**
  * 合并转发消息
- * @suppress 此 API 不稳定
+ * @suppress 此 API 非常不稳定
  */
-@SinceMirai("0.36.0")
-@MiraiExperimentalAPI
-class ForwardMessage(content: String) : ServiceMessage(35, content) {
-    companion object Key : Message.Key<XmlMessage> {
-        override val typeName: String get() = "ForwardMessage"
-    }
-}
+@SinceMirai("0.39.0")
+@MiraiExperimentalAPI("此 API 非常不稳定")
+internal class ForwardMessageInternal(content: String) : ServiceMessage(35, content)
 
 /*
 commonElem=CommonElem#750141174 {