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 {