From 5857d1b2ee4e229fdb0f9629f7b93e71c7223871 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 1 Feb 2021 13:14:18 +0800 Subject: [PATCH 01/13] Add MultiMsg.ApplyDown --- .../network/protocol/data/proto/MultiMsg.kt | 2 +- .../network/protocol/packet/PacketFactory.kt | 1 + .../network/protocol/packet/chat/MultiMsg.kt | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt index f75c5800d..378c87995 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt @@ -23,7 +23,7 @@ internal class MultiMsg : ProtoBuf { @Serializable internal class MultiMsgApplyDownReq( - @ProtoNumber(1) @JvmField val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(1) @JvmField val msgResid: String = "", @ProtoNumber(2) @JvmField val msgType: Int = 0, @ProtoNumber(3) @JvmField val srcUin: Long = 0L ) : ProtoBuf diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index b1b6d792f..ca5f26dab 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -153,6 +153,7 @@ internal object KnownPacketFactories { Heartbeat.Alive, PbMessageSvc.PbMsgWithDraw, MultiMsg.ApplyUp, + MultiMsg.ApplyDown, NewContact.SystemMsgNewFriend, NewContact.SystemMsgNewGroup, ProfileService.GroupMngReq, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt index 921f95ba4..6f62fa84c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt @@ -151,4 +151,41 @@ internal class MultiMsg { } } } + + object ApplyDown: OutgoingPacketFactory("MultiMsg.ApplyDown") { + sealed class Response : Packet { + + object MessageTooLarge : Response() + } + + operator fun invoke( + client: QQAndroidClient, + buType: Int, + resId: String, + msgType: Int, + ) = buildOutgoingUniPacket(client) { + writeProtoBuf( + MultiMsg.ReqBody.serializer(), + MultiMsg.ReqBody( + buType = buType, // 1: long, 2: 合并转发 + buildVer = "8.2.0.1296", + multimsgApplydownReq = listOf( + MultiMsg.MultiMsgApplyDownReq( + msgResid = resId, + msgType = msgType, + ) + ), + netType = 3, // wifi=3, wap=5 + platformType = 9, + subcmd = 2, + termType = 5, + reqChannelType = 2 + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { + return Response.MessageTooLarge + } + } } \ No newline at end of file From 3ff7fa7db1069b5bde257ddfb25b9adb706f8e95 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 1 Feb 2021 13:14:42 +0800 Subject: [PATCH 02/13] Decode service message as LongMessageInternal or ForwardMessageInternal if possible --- .../src/commonMain/kotlin/message/conversions.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/message/conversions.kt b/mirai-core/src/commonMain/kotlin/message/conversions.kt index c3a8d10e4..b957d2a09 100644 --- a/mirai-core/src/commonMain/kotlin/message/conversions.kt +++ b/mirai-core/src/commonMain/kotlin/message/conversions.kt @@ -558,11 +558,9 @@ internal fun List.joinToMessageChain( val resId = this.firstIsInstanceOrNull()?.longTextResid if (resId != null) { - // TODO: 2020/4/29 解析长消息 - list.add(SimpleServiceMessage(35, content)) // resId + list.add(LongMessageInternal(content, resId)) } else { - // TODO: 2020/4/29 解析合并转发 - list.add(SimpleServiceMessage(35, content)) + list.add(ForwardMessageInternal(content)) } } @@ -655,7 +653,10 @@ internal fun contextualBugReportException( e: Throwable? = null, additional: String = "" ): IllegalStateException { - return IllegalStateException("在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", e) + return IllegalStateException( + "在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", + e + ) } @OptIn(ExperimentalContracts::class) From 1768872bab5b8d90a92178ee1f6a684d18160658 Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 10:58:02 +0800 Subject: [PATCH 03/13] Rewrite message to struct conversions, add ReceiveMessageTransformer, for parsing long and forward messages in the future --- .../kotlin/message/data/MessageSource.kt | 2 +- .../src/commonMain/kotlin/StandardUtils.kt | 7 + .../commonMain/kotlin/contact/GroupImpl.kt | 3 +- .../kotlin/message/MessageSourceInternal.kt | 48 ++ .../kotlin/message/ReceiveMessageHandler.kt | 413 +++++++++++ .../message/contextualBugReportException.kt | 41 ++ .../commonMain/kotlin/message/conversions.kt | 674 ------------------ .../kotlin/message/incomingSourceImpl.kt | 210 +++--- .../kotlin/message/messageToElems.kt | 289 ++++++++ .../kotlin/message/offlineSourceImpl.kt | 28 +- .../chat/receive/MessageSvc.PbGetMsg.kt | 38 +- .../chat/receive/OnlinePush.PbPushGroupMsg.kt | 16 +- 12 files changed, 911 insertions(+), 858 deletions(-) create mode 100644 mirai-core/src/commonMain/kotlin/message/MessageSourceInternal.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt delete mode 100644 mirai-core/src/commonMain/kotlin/message/conversions.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/messageToElems.kt diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt index 49ebe45cb..3d55bc281 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt @@ -201,7 +201,7 @@ public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle { */ @JvmStatic @JvmBlockingBridge - public suspend inline fun MessageChain.recall(): Unit = this.source.recall() + public suspend fun MessageChain.recall(): Unit = this.source.recall() /** * 在一段时间后撤回这条消息. diff --git a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt index be1a5379a..fcaa9b38d 100644 --- a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt @@ -20,6 +20,13 @@ public inline fun Any?.safeCast(): T? = this as? T public inline fun Any?.castOrNull(): T? = this as? T +public inline fun Iterable<*>.firstIsInstanceOrNull(): R? { + for (it in this) { + if (it is R) return it + } + return null +} + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") @kotlin.internal.InlineOnly diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 980318085..8ce574f5f 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -46,9 +46,10 @@ internal fun GroupImpl.Companion.checkIsInstance(instance: Group) { check(instance is GroupImpl) { "group is not an instanceof GroupImpl!! DO NOT interlace two or more protocol implementations!!" } } -internal fun Group.checkIsGroupImpl() { +internal fun Group.checkIsGroupImpl(): GroupImpl { contract { returns() implies (this@checkIsGroupImpl is GroupImpl) } GroupImpl.checkIsInstance(this) + return this } @Suppress("PropertyName") diff --git a/mirai-core/src/commonMain/kotlin/message/MessageSourceInternal.kt b/mirai-core/src/commonMain/kotlin/message/MessageSourceInternal.kt new file mode 100644 index 000000000..c7edd6b64 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/MessageSourceInternal.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 + */ + +package net.mamoe.mirai.internal.message + +import kotlinx.serialization.Transient +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSource +import net.mamoe.mirai.message.data.sourceOrNull +import java.util.concurrent.atomic.AtomicBoolean + + +internal interface MessageSourceInternal { + @Transient + val sequenceIds: IntArray // ids + + @Transient + val internalIds: IntArray // randomId + + @Deprecated("don't use this internally. Use sequenceId or random instead.", level = DeprecationLevel.ERROR) + @Transient + val ids: IntArray + + @Transient + val isRecalledOrPlanned: AtomicBoolean + + fun toJceData(): ImMsgBody.SourceMsg +} + +@Suppress("RedundantSuspendModifier", "unused") +internal suspend fun MessageSource.ensureSequenceIdAvailable() { + if (this is OnlineMessageSourceToGroupImpl) { + ensureSequenceIdAvailable() + } +} + +@Suppress("RedundantSuspendModifier", "unused") +internal suspend inline fun Message.ensureSequenceIdAvailable() { + (this as? MessageChain)?.sourceOrNull?.ensureSequenceIdAvailable() +} diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt new file mode 100644 index 000000000..eab697c50 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -0,0 +1,413 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.message + +import kotlinx.io.core.discardExact +import kotlinx.io.core.readUInt +import net.mamoe.mirai.Bot +import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements +import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain +import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toVoice +import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFace +import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.* + +internal fun ImMsgBody.SourceMsg.toMessageChainNoSource( + botId: Long, + messageSourceKind: MessageSourceKind, + groupIdOrZero: Long +): MessageChain { + val elements = this.elems + return buildMessageChain(elements.size + 1) { + joinToMessageChain(elements, groupIdOrZero, messageSourceKind, botId, this) + }.cleanupRubbishMessageElements() +} + + +internal fun List.toMessageChainOnline( + bot: Bot, + groupIdOrZero: Long, + messageSourceKind: MessageSourceKind +): MessageChain { + return toMessageChain(bot, bot.id, groupIdOrZero, true, messageSourceKind) +} + +internal fun List.toMessageChainOffline( + bot: Bot, + groupIdOrZero: Long, + messageSourceKind: MessageSourceKind +): MessageChain { + return toMessageChain(bot, bot.id, groupIdOrZero, false, messageSourceKind) +} + +internal fun List.toMessageChainNoSource( + botId: Long, + groupIdOrZero: Long, + messageSourceKind: MessageSourceKind +): MessageChain { + return toMessageChain(null, botId, groupIdOrZero, null, messageSourceKind) +} + +private fun List.toMessageChain( + bot: Bot?, + botId: Long, + groupIdOrZero: Long, + onlineSource: Boolean?, + messageSourceKind: MessageSourceKind +): MessageChain { + val messageList = this + + + val elements = messageList.flatMap { it.msgBody.richText.elems } + + val builder = MessageChainBuilder(elements.size) + + if (onlineSource != null) { + checkNotNull(bot) + builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList)) + } + + joinToMessageChain(elements, groupIdOrZero, messageSourceKind, botId, builder) + + for (msg in messageList) { + msg.msgBody.richText.ptt?.toVoice()?.let { builder.add(it) } + } + + return builder.build().cleanupRubbishMessageElements() +} + +private object ReceiveMessageTransformer { + fun createMessageSource( + bot: Bot, + onlineSource: Boolean, + messageSourceKind: MessageSourceKind, + messageList: List, + ): MessageSource { + return when (onlineSource) { + true -> { + when (messageSourceKind) { + MessageSourceKind.TEMP -> OnlineMessageSourceFromTempImpl(bot, messageList) + MessageSourceKind.GROUP -> OnlineMessageSourceFromGroupImpl(bot, messageList) + MessageSourceKind.FRIEND -> OnlineMessageSourceFromFriendImpl(bot, messageList) + MessageSourceKind.STRANGER -> OnlineMessageSourceFromStrangerImpl(bot, messageList) + } + } + false -> { + OfflineMessageSourceImplData(bot, messageList, messageSourceKind) + } + } + } + + fun joinToMessageChain( + elements: List, + groupIdOrZero: Long, + messageSourceKind: MessageSourceKind, + botId: Long, + builder: MessageChainBuilder + ) { + // (this._miraiContentToString().soutv()) + val generalFlags = elements.find { it.generalFlags != null }?.generalFlags + + for (element in elements) { + transformElement(element, groupIdOrZero, messageSourceKind, botId, builder) + when { + element.richMsg != null -> decodeRichMessage(generalFlags, element.richMsg, builder) + } + } + } + + private fun transformElement( + element: ImMsgBody.Elem, + groupIdOrZero: Long, + messageSourceKind: MessageSourceKind, + botId: Long, + builder: MessageChainBuilder + ) { + when { + element.srcMsg != null -> decodeSrcMsg(element.srcMsg, builder, botId, messageSourceKind, groupIdOrZero) + element.notOnlineImage != null -> builder.add(OnlineFriendImageImpl(element.notOnlineImage)) + element.customFace != null -> decodeCustomFace(element.customFace, builder) + element.face != null -> builder.add(Face(element.face.index)) + element.text != null -> decodeText(element.text, builder) + element.marketFace != null -> builder.add(MarketFaceImpl(element.marketFace)) + element.lightApp != null -> decodeLightApp(element.lightApp, builder) + element.customElem != null -> decodeCustomElem(element.customElem, builder) + element.commonElem != null -> decodeCommonElem(element.commonElem, builder) + + element.elemFlags2 != null + || element.extraInfo != null + || element.generalFlags != null -> { + // ignore + } + else -> { + // println(it._miraiContentToString()) + } + } + } + + fun MessageChain.cleanupRubbishMessageElements(): MessageChain { + var previousLast: SingleMessage? = null + var last: SingleMessage? = null + return buildMessageChain(initialSize = this.count()) { + this@cleanupRubbishMessageElements.forEach { element -> + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + if (last is LongMessageInternal && element is PlainText) { + if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { + previousLast = last + last = element + return@forEach + } + } + if (last is PokeMessage && element is PlainText) { + if (element == UNSUPPORTED_POKE_MESSAGE_PLAIN) { + previousLast = last + last = element + return@forEach + } + } + if (last is VipFace && element is PlainText) { + val l = last as VipFace + if (element.content.length == 4 + (l.count / 10) + l.kind.name.length) { + previousLast = last + last = element + return@forEach + } + } + // 解决tim发送的语音无法正常识别 + if (element is PlainText) { + if (element == UNSUPPORTED_VOICE_MESSAGE_PLAIN) { + previousLast = last + last = element + return@forEach + } + } + + if (element is PlainText && last is At && previousLast is QuoteReply + && element.content.startsWith(' ') + ) { + // Android QQ 发送, 是 Quote+At+PlainText(" xxx") // 首空格 + removeLastOrNull() // At + val new = PlainText(element.content.substring(1)) + add(new) + previousLast = null + last = new + return@forEach + } + + if (element is QuoteReply) { + // 客户端为兼容早期不支持 QuoteReply 的客户端而添加的 At + removeLastOrNull()?.let { rm -> + if ((rm as? PlainText)?.content != " ") add(rm) + else removeLastOrNull()?.let { rm2 -> + if (rm2 !is At) add(rm2) + } + } + } + + if (element is PlainText) { // 处理分片消息 + append(element.content) + } else { + add(element) + } + + previousLast = last + last = element + } + } + } + + private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) { + if (text.attr6Buf.isEmpty()) { + list.add(PlainText(text.str)) + } else { + val id: Long + text.attr6Buf.read { + discardExact(7) + id = readUInt().toLong() + } + if (id == 0L) { + list.add(AtAll) + } else { + list.add(At(id)) // element.text.str + } + } + } + + private fun decodeSrcMsg( + srcMsg: ImMsgBody.SourceMsg, + list: MessageChainBuilder, + botId: Long, + messageSourceKind: MessageSourceKind, + groupIdOrZero: Long + ) { + list.add(QuoteReply(OfflineMessageSourceImplData(srcMsg, botId, messageSourceKind, groupIdOrZero))) + } + + private fun decodeCustomFace( + customFace: ImMsgBody.CustomFace, + builder: MessageChainBuilder, + ) { + builder.add(OnlineGroupImageImpl(customFace)) + customFace.pbReserve.let { + if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) { + builder.add(ShowImageFlag) + } + } + } + + private fun decodeLightApp( + lightApp: ImMsgBody.LightAppElem, + list: MessageChainBuilder + ) { + val content = runWithBugReport("解析 lightApp", + { "resId=" + lightApp.msgResid + "data=" + lightApp.data.toUHexString() }) { + when (lightApp.data[0].toInt()) { + 0 -> lightApp.data.encodeToString(offset = 1) + 1 -> lightApp.data.unzip(1).encodeToString() + else -> error("unknown compression flag=${lightApp.data[0]}") + } + } + + list.add(LightApp(content).refine()) + } + + private fun decodeCustomElem( + customElem: ImMsgBody.CustomElem, + list: MessageChainBuilder + ) { + customElem.data.read { + kotlin.runCatching { + CustomMessage.load(this) + }.fold( + onFailure = { + if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) { + throw IllegalStateException( + "Internal error: " + + "exception while deserializing CustomMessage head data," + + " data=${customElem.data.toUHexString()}", it + ) + } else { + it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException + throw IllegalStateException( + "User error: " + + "exception while deserializing CustomMessage body," + + " body=${it.body.toUHexString()}", it + ) + } + + }, + onSuccess = { + if (it != null) { + list.add(it) + } + } + ) + } + } + + private fun decodeCommonElem( + commonElem: ImMsgBody.CommonElem, + list: MessageChainBuilder + ) { + when (commonElem.serviceType) { + 23 -> { + val proto = + commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer()) + list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount)) + } + 2 -> { + val proto = + commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer()) + list.add(PokeMessage( + proto.vaspokeName.takeIf { it.isNotEmpty() } + ?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name + .orEmpty(), + proto.pokeType, + proto.vaspokeId + ) + ) + } + 3 -> { + val proto = + commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer()) + if (proto.flashTroopPic != null) { + list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic))) + } + if (proto.flashC2cPic != null) { + list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic))) + } + } + 33 -> { + val proto = + commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer()) + list.add(Face(proto.index)) + + } + } + } + + private fun decodeRichMessage( + generalFlags: ImMsgBody.GeneralFlags?, + richMsg: ImMsgBody.RichMsg, + builder: MessageChainBuilder + ) { + val content = runWithBugReport("解析 richMsg", { richMsg.template1.toUHexString() }) { + when (richMsg.template1[0].toInt()) { + 0 -> richMsg.template1.encodeToString(offset = 1) + 1 -> richMsg.template1.unzip(1).encodeToString() + else -> error("unknown compression flag=${richMsg.template1[0]}") + } + } + when (richMsg.serviceId) { + // 5: 使用微博长图转换功能分享到QQ群 + /* + + */ + /** + * json? + */ + 1 -> @Suppress("DEPRECATION_ERROR") + builder.add(SimpleServiceMessage(1, content)) + /** + * [LongMessageInternal], [ForwardMessage] + */ + 35 -> { + val resId = generalFlags?.longTextResid + + if (resId != null) { + builder.add(LongMessageInternal(content, resId)) + } else { + builder.add(ForwardMessageInternal(content)) + } + } + + // 104 新群员入群的消息 + else -> { + builder.add(SimpleServiceMessage(richMsg.serviceId, content)) + } + } + } + + fun ImMsgBody.Ptt.toVoice() = Voice( + kotlinx.io.core.String(fileName), + fileMd5, + fileSize.toLong(), + format, + kotlinx.io.core.String(downPara) + ) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt new file mode 100644 index 000000000..07ec8d051 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt @@ -0,0 +1,41 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.message + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + + +internal fun contextualBugReportException( + context: String, + forDebug: String, + e: Throwable? = null, + additional: String = "" +): IllegalStateException { + return IllegalStateException( + "在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", + e + ) +} + +@OptIn(ExperimentalContracts::class) +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +internal inline fun runWithBugReport(context: String, forDebug: () -> String, block: () -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + callsInPlace(forDebug, InvocationKind.AT_MOST_ONCE) + } + + return runCatching(block).getOrElse { + throw contextualBugReportException(context, forDebug(), it) + } +} diff --git a/mirai-core/src/commonMain/kotlin/message/conversions.kt b/mirai-core/src/commonMain/kotlin/message/conversions.kt deleted file mode 100644 index b957d2a09..000000000 --- a/mirai-core/src/commonMain/kotlin/message/conversions.kt +++ /dev/null @@ -1,674 +0,0 @@ -/* - * 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/master/LICENSE - */ - -@file:OptIn(LowLevelApi::class) -@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR") - -package net.mamoe.mirai.internal.message - -import kotlinx.io.core.String -import kotlinx.io.core.discardExact -import kotlinx.io.core.readUInt -import kotlinx.io.core.toByteArray -import net.mamoe.mirai.Bot -import net.mamoe.mirai.LowLevelApi -import net.mamoe.mirai.contact.AnonymousMember -import net.mamoe.mirai.contact.ContactOrBot -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.User -import net.mamoe.mirai.internal.network.protocol.data.proto.* -import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody -import net.mamoe.mirai.internal.utils.* -import net.mamoe.mirai.internal.utils.io.serialization.loadAs -import net.mamoe.mirai.internal.utils.io.serialization.toByteArray -import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.* -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - - -private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。") -private val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。") -private val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。") -private val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com") - -@OptIn(ExperimentalStdlibApi::class) -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -internal fun MessageChain.toRichTextElems( - messageTarget: ContactOrBot?, - withGeneralFlags: Boolean -): MutableList { - val forGroup = messageTarget is Group - val elements = ArrayList(this.size) - - if (this.anyIsInstance()) { - when (val source = this[QuoteReply]!!.source) { - is MessageSourceInternal -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData())) - else -> error("unsupported MessageSource implementation: ${source::class.simpleName}. Don't implement your own MessageSource.") - } - } - - var longTextResId: String? = null - - fun transformOneMessage(currentMessage: Message) { - if (currentMessage is RichMessage) { - val content = currentMessage.content.toByteArray().zip() - when (currentMessage) { - is ForwardMessageInternal -> { - elements.add( - ImMsgBody.Elem( - richMsg = ImMsgBody.RichMsg( - serviceId = currentMessage.serviceId, // ok - template1 = byteArrayOf(1) + content - ) - ) - ) - transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) - } - is LongMessageInternal -> { - check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" } - elements.add( - ImMsgBody.Elem( - richMsg = ImMsgBody.RichMsg( - serviceId = currentMessage.serviceId, // ok - template1 = byteArrayOf(1) + content - ) - ) - ) - transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) - longTextResId = currentMessage.resId - } - is LightApp -> elements.add( - ImMsgBody.Elem( - lightApp = ImMsgBody.LightAppElem( - data = byteArrayOf(1) + content - ) - ) - ) - else -> elements.add( - ImMsgBody.Elem( - richMsg = ImMsgBody.RichMsg( - serviceId = when (currentMessage) { - is ServiceMessage -> currentMessage.serviceId - else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}") - }, - template1 = byteArrayOf(1) + content - ) - ) - ) - } - } - - when (currentMessage) { - is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content))) - is CustomMessage -> { - @Suppress("UNCHECKED_CAST") - elements.add( - ImMsgBody.Elem( - customElem = ImMsgBody.CustomElem( - enumType = MIRAI_CUSTOM_ELEM_TYPE, - data = CustomMessage.dump( - currentMessage.getFactory() as CustomMessage.Factory, - currentMessage - ) - ) - ) - ) - } - is At -> { - elements.add(ImMsgBody.Elem(text = currentMessage.toJceData(messageTarget.safeCast()))) - // elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) - // removed by https://github.com/mamoe/mirai/issues/524 - // 发送 QuoteReply 消息时无可避免的产生多余空格 #524 - } - is PokeMessage -> { - elements.add( - ImMsgBody.Elem( - commonElem = ImMsgBody.CommonElem( - serviceType = 2, - businessType = currentMessage.pokeType, - pbElem = HummerCommelem.MsgElemInfoServtype2( - pokeType = currentMessage.pokeType, - vaspokeId = currentMessage.id, - vaspokeMinver = "7.2.0", - vaspokeName = currentMessage.name - ).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer()) - ) - ) - ) - transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN) - } - - - is OfflineGroupImage -> { - if (messageTarget is User) { - elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage())) - } else { - elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData())) - } - } - is OnlineGroupImageImpl -> { - if (messageTarget is User) { - elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage())) - } else { - elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate)) - } - } - is OnlineFriendImageImpl -> { - if (messageTarget is User) { - elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate)) - } else { - elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace())) - } - } - is OfflineFriendImage -> { - if (messageTarget is User) { - elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData())) - } else { - elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace())) - } - } - - - is FlashImage -> elements.add(currentMessage.toJceData(messageTarget)) - .also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) } - - - is AtAll -> elements.add(atAllData) - is Face -> elements.add( - if (currentMessage.id >= 260) { - ImMsgBody.Elem(commonElem = currentMessage.toCommData()) - } else { - ImMsgBody.Elem(face = currentMessage.toJceData()) - } - ) - is QuoteReply -> { - if (forGroup) { - when (val source = currentMessage.source) { - is OnlineMessageSource.Incoming.FromGroup -> { - val sender0 = source.sender - if (sender0 !is AnonymousMember) - transformOneMessage(At(sender0)) - // transformOneMessage(PlainText(" ")) - // removed by https://github.com/mamoe/mirai/issues/524 - // 发送 QuoteReply 消息时无可避免的产生多余空格 #524 - } - } - } - } - is MarketFace -> { - if (currentMessage is MarketFaceImpl) { - elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate)) - } - //兼容信息 - transformOneMessage(PlainText(currentMessage.name)) - if (currentMessage is MarketFaceImpl) { - elements.add( - ImMsgBody.Elem( - extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1) - ) - ) - } - } - is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString())) - is PttMessage -> { - elements.add( - ImMsgBody.Elem( - extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1) - ) - ) - elements.add( - ImMsgBody.Elem( - elemFlags2 = ImMsgBody.ElemFlags2( - vipStatus = 1 - ) - ) - ) - } - is MusicShare -> { - // 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT. - // 发送消息时会被特殊处理 - transformOneMessage(PlainText(currentMessage.content)) - } - - is ForwardMessage, - is MessageSource, // mirai metadata only - is RichMessage // already transformed above - -> { - - } - is InternalFlagOnlyMessage, is ShowImageFlag -> { - // ignore - } - else -> error("unsupported message type: ${currentMessage::class.simpleName}") - } - } - this.forEach(::transformOneMessage) - - if (withGeneralFlags) { - when { - longTextResId != null -> { - elements.add( - ImMsgBody.Elem( - generalFlags = ImMsgBody.GeneralFlags( - longTextFlag = 1, - longTextResid = longTextResId!!, - pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes() - ) - ) - ) - } - this.anyIsInstance() -> { - elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE))) - } - this.anyIsInstance() -> { - // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00 - elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE))) - } - this.anyIsInstance() -> { - elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU))) - } - this.anyIsInstance() -> { - elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT))) - } - else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE))) - } - } - - return elements -} - -private val PB_RESERVE_FOR_RICH_MESSAGE = - "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes() - -private val PB_RESERVE_FOR_PTT = - "78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes() - -@Suppress("SpellCheckingInspection") -private val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes() -private val PB_RESERVE_FOR_MARKET_FACE = - "02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes() -private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes() - -internal fun MsgComm.Msg.toMessageChain( - bot: Bot, - groupIdOrZero: Long, - onlineSource: Boolean, - messageSourceKind: MessageSourceKind -): MessageChain = listOf(this).toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, messageSourceKind) - -internal fun List.toMessageChain( - bot: Bot, - groupIdOrZero: Long, - onlineSource: Boolean, - messageSourceKind: MessageSourceKind -): MessageChain = map { it.msg }.toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, messageSourceKind) - -internal fun List.toMessageChain( - bot: Bot?, - botId: Long, - groupIdOrZero: Long, - onlineSource: Boolean?, - messageSourceKind: MessageSourceKind -): MessageChain { - val elements = this.flatMap { it.msgBody.richText.elems } - - @OptIn(ExperimentalStdlibApi::class) - val ptts = buildList { - this@toMessageChain.forEach { msg -> - msg.msgBody.richText.ptt?.run { -// when (fileType) { -// 4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara)) -// else -> null -// } - add(Voice(String(fileName), fileMd5, fileSize.toLong(), format, String(downPara))) - } - } - } - return buildMessageChain(elements.size + 1 + ptts.size) { - when (onlineSource) { - true -> { - checkNotNull(bot) { "bot is null" } - - when (messageSourceKind) { - MessageSourceKind.TEMP -> +OnlineMessageSourceFromTempImpl(bot, this@toMessageChain) - MessageSourceKind.GROUP -> +OnlineMessageSourceFromGroupImpl(bot, this@toMessageChain) - MessageSourceKind.FRIEND -> +OnlineMessageSourceFromFriendImpl(bot, this@toMessageChain) - MessageSourceKind.STRANGER -> +OnlineMessageSourceFromStrangerImpl(bot, this@toMessageChain) - } - } - false -> { - +OfflineMessageSourceImplData(bot, this@toMessageChain, botId) - } - null -> { - - } - } - elements.joinToMessageChain(groupIdOrZero, messageSourceKind, botId, this) - addAll(ptts) - }.cleanupRubbishMessageElements() -} - -// These two functions have difference method signature, don't combine. - -internal fun ImMsgBody.SourceMsg.toMessageChain( - botId: Long, - messageSourceKind: MessageSourceKind, - groupIdOrZero: Long -): MessageChain { - val elements = this.elems - if (elements.isEmpty()) - error("elements for SourceMsg is empty") - return buildMessageChain(elements.size + 1) { - /* - +OfflineMessageSourceImplData( - delegate = this@toMessageChain, - botId = botId, - messageSourceKind = messageSourceKind, - groupIdOrZero = groupIdOrZero - )*/ - elements.joinToMessageChain(groupIdOrZero, messageSourceKind, botId, this) - }.cleanupRubbishMessageElements() -} - -private fun MessageChain.cleanupRubbishMessageElements(): MessageChain { - var previousLast: SingleMessage? = null - var last: SingleMessage? = null - return buildMessageChain(initialSize = this.count()) { - this@cleanupRubbishMessageElements.forEach { element -> - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - if (last is LongMessageInternal && element is PlainText) { - if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { - previousLast = last - last = element - return@forEach - } - } - if (last is PokeMessage && element is PlainText) { - if (element == UNSUPPORTED_POKE_MESSAGE_PLAIN) { - previousLast = last - last = element - return@forEach - } - } - if (last is VipFace && element is PlainText) { - val l = last as VipFace - if (element.content.length == 4 + (l.count / 10) + l.kind.name.length) { - previousLast = last - last = element - return@forEach - } - } - // 解决tim发送的语音无法正常识别 - if (element is PlainText) { - if (element == UNSUPPORTED_VOICE_MESSAGE_PLAIN) { - previousLast = last - last = element - return@forEach - } - } - - if (element is PlainText && last is At && previousLast is QuoteReply - && element.content.startsWith(' ') - ) { - // Android QQ 发送, 是 Quote+At+PlainText(" xxx") // 首空格 - removeLastOrNull() // At - val new = PlainText(element.content.substring(1)) - add(new) - previousLast = null - last = new - return@forEach - } - - if (element is QuoteReply) { - // 客户端为兼容早期不支持 QuoteReply 的客户端而添加的 At - removeLastOrNull()?.let { rm -> - if ((rm as? PlainText)?.content != " ") add(rm) - else removeLastOrNull()?.let { rm2 -> - if (rm2 !is At) add(rm2) - } - } - } - - if (element is PlainText) { // 处理分片消息 - append(element.content) - } else { - add(element) - } - - previousLast = last - last = element - } - } -} - -internal inline fun Iterable<*>.firstIsInstanceOrNull(): R? { - for (it in this) { - if (it is R) { - return it - } - } - return null -} - -internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510 - -internal fun List.joinToMessageChain( - groupIdOrZero: Long, - messageSourceKind: MessageSourceKind, - botId: Long, - list: MessageChainBuilder -) { - // (this._miraiContentToString().soutv()) - var marketFace: MarketFaceImpl? = null - this.forEach { element -> - when { - element.srcMsg != null -> { - list.add( - QuoteReply( - OfflineMessageSourceImplData( - element.srcMsg, - botId, - messageSourceKind, - groupIdOrZero - ) - ) - ) - } - element.notOnlineImage != null -> list.add(OnlineFriendImageImpl(element.notOnlineImage)) - element.customFace != null -> { - list.add(OnlineGroupImageImpl(element.customFace)) - element.customFace.pbReserve.let { - if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) { - list.add(ShowImageFlag) - } - } - } - element.face != null -> list.add(Face(element.face.index)) - element.text != null -> { - if (element.text.attr6Buf.isEmpty()) { - if (marketFace != null && marketFace!!.name.isEmpty()) { - marketFace!!.delegate.faceName = element.text.str.toByteArray() - } else { - list.add(PlainText(element.text.str)) - } - } else { - val id: Long - element.text.attr6Buf.read { - discardExact(7) - id = readUInt().toLong() - } - if (id == 0L) { - list.add(AtAll) - } else { - list.add(At(id)) // element.text.str - } - } - } - element.marketFace != null -> { - list.add(MarketFaceImpl(element.marketFace).also { - marketFace = it - }) - } - element.lightApp != null -> { - val content = runWithBugReport("解析 lightApp", - { "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) { - when (element.lightApp.data[0].toInt()) { - 0 -> element.lightApp.data.encodeToString(offset = 1) - 1 -> element.lightApp.data.unzip(1).encodeToString() - else -> error("unknown compression flag=${element.lightApp.data[0]}") - } - } - - list.add(LightApp(content).refine()) - } - element.richMsg != null -> { - val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) { - when (element.richMsg.template1[0].toInt()) { - 0 -> element.richMsg.template1.encodeToString(offset = 1) - 1 -> element.richMsg.template1.unzip(1).encodeToString() - else -> error("unknown compression flag=${element.richMsg.template1[0]}") - } - } - when (element.richMsg.serviceId) { - // 5: 使用微博长图转换功能分享到QQ群 - /* - - */ - /** - * json? - */ - 1 -> @Suppress("DEPRECATION_ERROR") - list.add(SimpleServiceMessage(1, content)) - /** - * [LongMessageInternal], [ForwardMessage] - */ - 35 -> { - val resId = this.firstIsInstanceOrNull()?.longTextResid - - if (resId != null) { - list.add(LongMessageInternal(content, resId)) - } else { - list.add(ForwardMessageInternal(content)) - } - } - - // 104 新群员入群的消息 - else -> { - list.add(SimpleServiceMessage(element.richMsg.serviceId, content)) - } - } - } - element.elemFlags2 != null - || element.extraInfo != null - || element.generalFlags != null -> { - - } - element.customElem != null -> { - element.customElem.data.read { - kotlin.runCatching { - CustomMessage.load(this) - }.fold( - onFailure = { - if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) { - throw IllegalStateException( - "Internal error: " + - "exception while deserializing CustomMessage head data," + - " data=${element.customElem.data.toUHexString()}", it - ) - } else { - it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException - throw IllegalStateException( - "User error: " + - "exception while deserializing CustomMessage body," + - " body=${it.body.toUHexString()}", it - ) - } - - }, - onSuccess = { - if (it != null) { - list.add(it) - } - } - ) - } - } - element.commonElem != null -> { - when (element.commonElem.serviceType) { - 23 -> { - val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer()) - list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount)) - } - 2 -> { - val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer()) - list.add(PokeMessage( - proto.vaspokeName.takeIf { it.isNotEmpty() } - ?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name - .orEmpty(), - proto.pokeType, - proto.vaspokeId - ) - ) - } - 3 -> { - val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer()) - if (proto.flashTroopPic != null) { - list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic))) - } - if (proto.flashC2cPic != null) { - list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic))) - } - } - 33 -> { - val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer()) - list.add(Face(proto.index)) - - } - } - } - else -> { - // println(it._miraiContentToString()) - } - } - } - -} - - -internal fun contextualBugReportException( - context: String, - forDebug: String, - e: Throwable? = null, - additional: String = "" -): IllegalStateException { - return IllegalStateException( - "在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", - e - ) -} - -@OptIn(ExperimentalContracts::class) -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -internal inline fun runWithBugReport(context: String, forDebug: () -> String, block: () -> R): R { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - callsInPlace(forDebug, InvocationKind.AT_MOST_ONCE) - } - - return runCatching(block).getOrElse { - throw contextualBugReportException(context, forDebug(), it) - } -} diff --git a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt index d3f1c6486..fdab19beb 100644 --- a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt @@ -17,7 +17,7 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Stranger -import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.checkIsGroupImpl import net.mamoe.mirai.internal.contact.newAnonymous import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm @@ -25,66 +25,32 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.internal.utils.io.serialization.toByteArray -import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSourceKind +import net.mamoe.mirai.message.data.OnlineMessageSource import net.mamoe.mirai.utils.encodeToBase64 import net.mamoe.mirai.utils.encodeToString import net.mamoe.mirai.utils.mapToIntArray import java.util.concurrent.atomic.AtomicBoolean -internal interface MessageSourceInternal { - @Transient - val sequenceIds: IntArray // ids - - @Transient - val internalIds: IntArray // randomId - - @Deprecated("don't use this internally. Use sequenceId or random instead.", level = DeprecationLevel.ERROR) - @Transient - val ids: IntArray - - @Transient - val isRecalledOrPlanned: AtomicBoolean - - fun toJceData(): ImMsgBody.SourceMsg -} - -@Suppress("RedundantSuspendModifier", "unused") -internal suspend fun MessageSource.ensureSequenceIdAvailable() { - if (this is OnlineMessageSourceToGroupImpl) { - ensureSequenceIdAvailable() - } -} - -@Suppress("RedundantSuspendModifier", "unused") -internal suspend inline fun Message.ensureSequenceIdAvailable() { - (this as? MessageChain)?.sourceOrNull?.ensureSequenceIdAvailable() -} - @Serializable(OnlineMessageSourceFromFriendImpl.Serializer::class) internal class OnlineMessageSourceFromFriendImpl( override val bot: Bot, - val msg: List + msg: List ) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceInternal { object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromFriend") - override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq } + override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false) override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random - override val internalIds: IntArray - get() = msg.mapToIntArray { - it.msgBody.richText.attr?.random ?: 0 - } // other client 消息的这个是0 - override val time: Int get() = msg.first().msgHead.msgTime + override val internalIds: IntArray = msg.mapToIntArray { + it.msgBody.richText.attr?.random ?: 0 + } // other client 消息的这个是0 + override val time: Int = msg.first().msgHead.msgTime override val originalMessage: MessageChain by lazy { - msg.toMessageChain( - bot, - bot.id, - 0, - null, - MessageSourceKind.FRIEND - ) + msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.FRIEND) } - override val sender: Friend get() = bot.getFriendOrFail(msg.first().msgHead.fromUin) + override val sender: Friend = bot.getFriendOrFail(msg.first().msgHead.fromUin) private val jceData by lazy { msg.toJceDataPrivate(internalIds) } @@ -94,28 +60,21 @@ internal class OnlineMessageSourceFromFriendImpl( @Serializable(OnlineMessageSourceFromStrangerImpl.Serializer::class) internal class OnlineMessageSourceFromStrangerImpl( override val bot: Bot, - val msg: List + msg: List ) : OnlineMessageSource.Incoming.FromStranger(), MessageSourceInternal { object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromStranger") - override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq } + override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false) override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random - override val internalIds: IntArray - get() = msg.mapToIntArray { - it.msgBody.richText.attr?.random ?: 0 - } // other client 消息的这个是0 - override val time: Int get() = msg.first().msgHead.msgTime + override val internalIds: IntArray = msg.mapToIntArray { + it.msgBody.richText.attr?.random ?: 0 + } // other client 消息的这个是0 + override val time: Int = msg.first().msgHead.msgTime override val originalMessage: MessageChain by lazy { - msg.toMessageChain( - bot, - bot.id, - 0, - null, - MessageSourceKind.STRANGER - ) + msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.STRANGER) } - override val sender: Stranger get() = bot.getStrangerOrFail(msg.first().msgHead.fromUin) + override val sender: Stranger = bot.getStrangerOrFail(msg.first().msgHead.fromUin) private val jceData by lazy { msg.toJceDataPrivate(internalIds) } @@ -126,100 +85,101 @@ private fun List.toJceDataPrivate(ids: IntArray): ImMsgBody.SourceM val elements = flatMap { it.msgBody.richText.elems }.toMutableList().also { if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2())) } - return ImMsgBody.SourceMsg( - origSeqs = mapToIntArray { it.msgHead.msgSeq }, - senderUin = first().msgHead.fromUin, - toUin = first().msgHead.toUin, - flag = 1, - elems = flatMap { it.msgBody.richText.elems }, - type = 0, - time = this.first().msgHead.msgTime, - pbReserve = SourceMsg.ResvAttr( - origUids = ids.map { it.toLong() and 0xFFFF_FFFF } - ).toByteArray(SourceMsg.ResvAttr.serializer()), - srcMsg = MsgComm.Msg( - msgHead = MsgComm.MsgHead( - fromUin = this.first().msgHead.fromUin, // qq - toUin = this.first().msgHead.toUin, // group - msgType = this.first().msgHead.msgType, // 82? - c2cCmd = this.first().msgHead.c2cCmd, - msgSeq = this.first().msgHead.msgSeq, - msgTime = this.first().msgHead.msgTime, - msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok - // groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode), - isSrcMsg = true - ), - msgBody = ImMsgBody.MsgBody( - richText = ImMsgBody.RichText( - elems = elements + + first().msgHead.run { + return ImMsgBody.SourceMsg( + origSeqs = mapToIntArray { it.msgHead.msgSeq }, + senderUin = fromUin, + toUin = toUin, + flag = 1, + elems = flatMap { it.msgBody.richText.elems }, + type = 0, + time = msgTime, + pbReserve = SourceMsg.ResvAttr( + origUids = ids.map { it.toLong() and 0xFFFF_FFFF } + ).toByteArray(SourceMsg.ResvAttr.serializer()), + srcMsg = MsgComm.Msg( + msgHead = MsgComm.MsgHead( + fromUin = fromUin, // qq + toUin = toUin, // group + msgType = msgType, // 82? + c2cCmd = c2cCmd, + msgSeq = msgSeq, + msgTime = msgTime, + msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok + // groupInfo = MsgComm.GroupInfo(groupCode = msgHead.groupInfo.groupCode), + isSrcMsg = true + ), + msgBody = ImMsgBody.MsgBody( + richText = ImMsgBody.RichText( + elems = elements + ) ) - ) - ).toByteArray(MsgComm.Msg.serializer()) - ) + ).toByteArray(MsgComm.Msg.serializer()) + ) + } } @Serializable(OnlineMessageSourceFromTempImpl.Serializer::class) internal class OnlineMessageSourceFromTempImpl( override val bot: Bot, - private val msg: List + msg: List ) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal { object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromTemp") - override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq } - override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random } + override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } + override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random } override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false) override val ids: IntArray get() = sequenceIds// - override val time: Int get() = msg.first().msgHead.msgTime + override val time: Int = msg.first().msgHead.msgTime override val originalMessage: MessageChain by lazy { - msg.toMessageChain( - bot, - bot.id, - groupIdOrZero = 0, - onlineSource = null, - MessageSourceKind.TEMP - ) + msg.toMessageChainNoSource(bot.id, groupIdOrZero = 0, MessageSourceKind.TEMP) + } + override val sender: Member = with(msg.first().msgHead) { + bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin) } - override val sender: Member - get() = with(msg.first().msgHead) { - bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin) - } private val jceData by lazy { msg.toJceDataPrivate(internalIds) } override fun toJceData(): ImMsgBody.SourceMsg = jceData } @Serializable(OnlineMessageSourceFromGroupImpl.Serializer::class) -internal data class OnlineMessageSourceFromGroupImpl( +internal class OnlineMessageSourceFromGroupImpl( override val bot: Bot, - private val msg: List + msg: List ) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal { object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl") @Transient override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false) - override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq } - override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random } + override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq } + override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random } override val ids: IntArray get() = sequenceIds - override val time: Int get() = msg.first().msgHead.msgTime + override val time: Int = msg.first().msgHead.msgTime override val originalMessage: MessageChain by lazy { - msg.toMessageChain(bot, bot.id, groupIdOrZero = group.id, onlineSource = null, MessageSourceKind.GROUP) + msg.toMessageChainNoSource(bot.id, groupIdOrZero = group.id, MessageSourceKind.GROUP) } override val sender: Member by lazy { - (bot.getGroup( - msg.first().msgHead.groupInfo?.groupCode - ?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") - ) as GroupImpl).run { - get(msg.first().msgHead.fromUin) - ?: msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run { - newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64()) - } - ?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") + val groupCode = msg.first().msgHead.groupInfo?.groupCode + ?: error("cannot find groupCode for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") + + val group = bot.getGroup(groupCode)?.checkIsGroupImpl() + ?: error("cannot find group for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") + + val member = group[msg.first().msgHead.fromUin] + if (member != null) return@lazy member + + val anonymousInfo = msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null } + ?: error("cannot find member for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") + + anonymousInfo.run { + group.newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64()) } } - override fun toJceData(): ImMsgBody.SourceMsg { - return ImMsgBody.SourceMsg( + private val jceData by lazy { + ImMsgBody.SourceMsg( origSeqs = intArrayOf(msg.first().msgHead.msgSeq), senderUin = msg.first().msgHead.fromUin, toUin = 0, @@ -231,4 +191,8 @@ internal data class OnlineMessageSourceFromGroupImpl( srcMsg = EMPTY_BYTE_ARRAY ) } -} + + override fun toJceData(): ImMsgBody.SourceMsg { + return jceData + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/messageToElems.kt b/mirai-core/src/commonMain/kotlin/message/messageToElems.kt new file mode 100644 index 000000000..c5ada71f9 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/messageToElems.kt @@ -0,0 +1,289 @@ +/* + * 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 + */ + +package net.mamoe.mirai.internal.message + +import kotlinx.io.core.toByteArray +import net.mamoe.mirai.contact.AnonymousMember +import net.mamoe.mirai.contact.ContactOrBot +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.User +import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.hexToBytes +import net.mamoe.mirai.utils.safeCast +import net.mamoe.mirai.utils.zip + +internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510 + + +internal val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。") +internal val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。") +internal val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。") +internal val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com") + +@OptIn(ExperimentalStdlibApi::class) +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +internal fun MessageChain.toRichTextElems( + messageTarget: ContactOrBot?, + withGeneralFlags: Boolean +): MutableList { + val forGroup = messageTarget is Group + val elements = ArrayList(this.size) + + if (this.anyIsInstance()) { + when (val source = this[QuoteReply]!!.source) { + is MessageSourceInternal -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData())) + else -> error("unsupported MessageSource implementation: ${source::class.simpleName}. Don't implement your own MessageSource.") + } + } + + var longTextResId: String? = null + + fun transformOneMessage(currentMessage: Message) { + if (currentMessage is RichMessage) { + val content = currentMessage.content.toByteArray().zip() + when (currentMessage) { + is ForwardMessageInternal -> { + elements.add( + ImMsgBody.Elem( + richMsg = ImMsgBody.RichMsg( + serviceId = currentMessage.serviceId, // ok + template1 = byteArrayOf(1) + content + ) + ) + ) + transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) + } + is LongMessageInternal -> { + check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" } + elements.add( + ImMsgBody.Elem( + richMsg = ImMsgBody.RichMsg( + serviceId = currentMessage.serviceId, // ok + template1 = byteArrayOf(1) + content + ) + ) + ) + transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) + longTextResId = currentMessage.resId + } + is LightApp -> elements.add( + ImMsgBody.Elem( + lightApp = ImMsgBody.LightAppElem( + data = byteArrayOf(1) + content + ) + ) + ) + else -> elements.add( + ImMsgBody.Elem( + richMsg = ImMsgBody.RichMsg( + serviceId = when (currentMessage) { + is ServiceMessage -> currentMessage.serviceId + else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}") + }, + template1 = byteArrayOf(1) + content + ) + ) + ) + } + } + + when (currentMessage) { + is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content))) + is CustomMessage -> { + @Suppress("UNCHECKED_CAST") + elements.add( + ImMsgBody.Elem( + customElem = ImMsgBody.CustomElem( + enumType = MIRAI_CUSTOM_ELEM_TYPE, + data = CustomMessage.dump( + currentMessage.getFactory() as CustomMessage.Factory, + currentMessage + ) + ) + ) + ) + } + is At -> { + elements.add(ImMsgBody.Elem(text = currentMessage.toJceData(messageTarget.safeCast()))) + // elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) + // removed by https://github.com/mamoe/mirai/issues/524 + // 发送 QuoteReply 消息时无可避免的产生多余空格 #524 + } + is PokeMessage -> { + elements.add( + ImMsgBody.Elem( + commonElem = ImMsgBody.CommonElem( + serviceType = 2, + businessType = currentMessage.pokeType, + pbElem = HummerCommelem.MsgElemInfoServtype2( + pokeType = currentMessage.pokeType, + vaspokeId = currentMessage.id, + vaspokeMinver = "7.2.0", + vaspokeName = currentMessage.name + ).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer()) + ) + ) + ) + transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN) + } + + + is OfflineGroupImage -> { + if (messageTarget is User) { + elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage())) + } else { + elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData())) + } + } + is OnlineGroupImageImpl -> { + if (messageTarget is User) { + elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage())) + } else { + elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate)) + } + } + is OnlineFriendImageImpl -> { + if (messageTarget is User) { + elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate)) + } else { + elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace())) + } + } + is OfflineFriendImage -> { + if (messageTarget is User) { + elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData())) + } else { + elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace())) + } + } + + + is FlashImage -> elements.add(currentMessage.toJceData(messageTarget)) + .also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) } + + + is AtAll -> elements.add(atAllData) + is Face -> elements.add( + if (currentMessage.id >= 260) { + ImMsgBody.Elem(commonElem = currentMessage.toCommData()) + } else { + ImMsgBody.Elem(face = currentMessage.toJceData()) + } + ) + is QuoteReply -> { + if (forGroup) { + when (val source = currentMessage.source) { + is OnlineMessageSource.Incoming.FromGroup -> { + val sender0 = source.sender + if (sender0 !is AnonymousMember) + transformOneMessage(At(sender0)) + // transformOneMessage(PlainText(" ")) + // removed by https://github.com/mamoe/mirai/issues/524 + // 发送 QuoteReply 消息时无可避免的产生多余空格 #524 + } + } + } + } + is MarketFace -> { + if (currentMessage is MarketFaceImpl) { + elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate)) + } + //兼容信息 + transformOneMessage(PlainText(currentMessage.name)) + if (currentMessage is MarketFaceImpl) { + elements.add( + ImMsgBody.Elem( + extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1) + ) + ) + } + } + is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString())) + is PttMessage -> { + elements.add( + ImMsgBody.Elem( + extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1) + ) + ) + elements.add( + ImMsgBody.Elem( + elemFlags2 = ImMsgBody.ElemFlags2( + vipStatus = 1 + ) + ) + ) + } + is MusicShare -> { + // 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT. + // 发送消息时会被特殊处理 + transformOneMessage(PlainText(currentMessage.content)) + } + + is ForwardMessage, + is MessageSource, // mirai metadata only + is RichMessage // already transformed above + -> { + + } + is InternalFlagOnlyMessage, is ShowImageFlag -> { + // ignore + } + else -> error("unsupported message type: ${currentMessage::class.simpleName}") + } + } + this.forEach(::transformOneMessage) + + if (withGeneralFlags) { + when { + longTextResId != null -> { + elements.add( + ImMsgBody.Elem( + generalFlags = ImMsgBody.GeneralFlags( + longTextFlag = 1, + longTextResid = longTextResId!!, + pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes() + ) + ) + ) + } + this.anyIsInstance() -> { + elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE))) + } + this.anyIsInstance() -> { + // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00 + elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE))) + } + this.anyIsInstance() -> { + elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU))) + } + this.anyIsInstance() -> { + elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT))) + } + else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE))) + } + } + + return elements +} + +internal val PB_RESERVE_FOR_RICH_MESSAGE = + "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes() + +internal val PB_RESERVE_FOR_PTT = + "78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes() + +@Suppress("SpellCheckingInspection") +internal val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes() +internal val PB_RESERVE_FOR_MARKET_FACE = + "02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes() +internal val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes() diff --git a/mirai-core/src/commonMain/kotlin/message/offlineSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/offlineSourceImpl.kt index 80754a122..edfc3db31 100644 --- a/mirai-core/src/commonMain/kotlin/message/offlineSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/offlineSourceImpl.kt @@ -106,40 +106,24 @@ internal class OfflineMessageSourceImplData( } internal fun OfflineMessageSourceImplData( - bot: Bot?, + bot: Bot, delegate: List, - botId: Long, + kind: MessageSourceKind ): OfflineMessageSourceImplData { val head = delegate.first().msgHead - val kind = when { - head.groupInfo != null -> { - MessageSourceKind.GROUP - } - head.c2cTmpMsgHead != null -> { - MessageSourceKind.TEMP - } - bot?.getStranger(head.fromUin) != null -> { - MessageSourceKind.STRANGER - } - else -> { - MessageSourceKind.FRIEND - } - } return OfflineMessageSourceImplData( kind = kind, time = head.msgTime, fromId = head.fromUin, targetId = head.groupInfo?.groupCode ?: head.toUin, - originalMessage = delegate.toMessageChain( - null, - botId, + originalMessage = delegate.toMessageChainNoSource( + bot.id, groupIdOrZero = head.groupInfo?.groupCode ?: 0, - onlineSource = null, messageSourceKind = kind ), ids = delegate.mapToIntArray { it.msgHead.msgSeq }, internalIds = delegate.mapToIntArray { it.msgHead.msgUid.toInt() }, - botId = botId + botId = bot.id ).apply { originElems = delegate.flatMap { it.msgBody.richText.elems } } @@ -177,7 +161,7 @@ internal fun OfflineMessageSourceImplData( internalIds = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()) .origUids?.mapToIntArray { it.toInt() } ?: intArrayOf(), time = delegate.time, - originalMessageLazy = lazy { delegate.toMessageChain(botId, messageSourceKind, groupIdOrZero) }, + originalMessageLazy = lazy { delegate.toMessageChainNoSource(botId, messageSourceKind, groupIdOrZero) }, fromId = delegate.senderUin, targetId = when { groupIdOrZero != 0L -> groupIdOrZero diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt index 0cf368651..e82339dff 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt @@ -30,7 +30,7 @@ import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.message.OnlineMessageSourceFromFriendImpl -import net.mamoe.mirai.internal.message.toMessageChain +import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.MultiPacket import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient @@ -295,7 +295,7 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean // 有人被邀请(经过同意后)加入 27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 34 30 34 38 32 33 38 35 37 41 37 38 46 33 45 37 35 38 42 39 38 46 43 45 44 43 32 41 30 31 36 36 30 34 31 36 39 35 39 30 38 39 30 39 45 31 34 34 // 搜索到群, 直接加入 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35 - } + } 34 -> { // 与 33 重复 return null @@ -399,8 +399,8 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean if (fromSync) { FriendMessageSyncEvent( friend, - msgs.toMessageChain( - bot = bot, botId = bot.id, groupIdOrZero = 0, onlineSource = true, + msgs.toMessageChainOnline( + bot = bot, groupIdOrZero = 0, messageSourceKind = MessageSourceKind.FRIEND ), msgHead.msgTime @@ -408,9 +408,9 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean } else { FriendMessageEvent( friend, - msgs.toMessageChain( - bot = bot, botId = bot.id, groupIdOrZero = 0, onlineSource = true, - messageSourceKind = MessageSourceKind.FRIEND + msgs.toMessageChainOnline( + bot = bot, groupIdOrZero = 0, + MessageSourceKind.FRIEND ), msgHead.msgTime ) @@ -430,13 +430,13 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean if (fromSync) { StrangerMessageSyncEvent( stranger, - toMessageChain(bot, groupIdOrZero = 0, onlineSource = true, MessageSourceKind.STRANGER), + listOf(this).toMessageChainOnline(bot, groupIdOrZero = 0, MessageSourceKind.STRANGER), msgHead.msgTime ) } else { StrangerMessageEvent( stranger, - toMessageChain(bot, groupIdOrZero = 0, onlineSource = true, MessageSourceKind.STRANGER), + listOf(this).toMessageChainOnline(bot, groupIdOrZero = 0, MessageSourceKind.STRANGER), msgHead.msgTime ) } @@ -507,26 +507,16 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean member.lastMessageSequence.loop { instant -> if (msgHead.msgSeq > instant) { if (member.lastMessageSequence.compareAndSet(instant, msgHead.msgSeq)) { - if (fromSync) { - return GroupTempMessageSyncEvent( + return if (fromSync) { + GroupTempMessageSyncEvent( member, - toMessageChain( - bot, - groupIdOrZero = 0, - onlineSource = true, - MessageSourceKind.TEMP - ), + listOf(this).toMessageChainOnline(bot, 0, MessageSourceKind.TEMP), msgHead.msgTime ) } else { - return GroupTempMessageEvent( + GroupTempMessageEvent( member, - toMessageChain( - bot, - groupIdOrZero = 0, - onlineSource = true, - MessageSourceKind.TEMP - ), + listOf(this).toMessageChainOnline(bot, 0, MessageSourceKind.TEMP), msgHead.msgTime ) } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt index 00ea8099c..982de268b 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt @@ -23,7 +23,7 @@ import net.mamoe.mirai.event.events.GroupMessageSyncEvent import net.mamoe.mirai.event.events.MemberCardChangeEvent import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.* -import net.mamoe.mirai.internal.message.toMessageChain +import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm @@ -119,12 +119,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin if (isFromSelfAccount) { return GroupMessageSyncEvent( - message = msgs.toMessageChain( - bot, - groupIdOrZero = group.id, - onlineSource = true, - MessageSourceKind.GROUP - ), + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP,), time = msgHead.msgTime, group = group, sender = sender, @@ -137,12 +132,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin return GroupMessageEvent( senderName = name, sender = sender, - message = msgs.toMessageChain( - bot, - groupIdOrZero = group.id, - onlineSource = true, - MessageSourceKind.GROUP - ), + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), permission = findMemberPermission(extraInfo?.flags ?: 0, sender, bot), time = msgHead.msgTime ) From de8e6469e073238edeb73c0c0c809457ae90924d Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 14:51:29 +0800 Subject: [PATCH 04/13] Support long message refinement, close #692 --- .../src/commonMain/kotlin/QQAndroidBot.kt | 1 + .../commonMain/kotlin/contact/AbstractUser.kt | 4 +- .../commonMain/kotlin/contact/GroupImpl.kt | 2 +- .../kotlin/message/LongMessageInternal.kt | 99 +++++++++++++++++-- .../kotlin/message/ReceiveMessageHandler.kt | 20 +++- .../kotlin/network/highway/Highway.kt | 59 ++++++++++- .../protocol/data/proto/MsgTransmit.kt | 8 +- .../network/protocol/data/proto/MultiMsg.kt | 4 +- .../network/protocol/packet/chat/MultiMsg.kt | 23 ++++- .../chat/receive/MessageSvc.PbGetMsg.kt | 28 +++--- .../chat/receive/OnlinePush.PbPushGroupMsg.kt | 7 +- 11 files changed, 215 insertions(+), 40 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index 6756d319c..ec53ede39 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -57,6 +57,7 @@ internal class QQAndroidBot constructor( configuration: BotConfiguration ) : AbstractBot(configuration, account.id) { var client: QQAndroidClient = initClient() + private set fun initClient(): QQAndroidClient { client = QQAndroidClient( diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt index 0b9da3e04..579a71ee3 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt @@ -22,7 +22,7 @@ import net.mamoe.mirai.internal.network.highway.ChannelKind import net.mamoe.mirai.internal.network.highway.Highway import net.mamoe.mirai.internal.network.highway.ResourceKind.PRIVATE_IMAGE import net.mamoe.mirai.internal.network.highway.postImage -import net.mamoe.mirai.internal.network.highway.tryServers +import net.mamoe.mirai.internal.network.highway.tryServersUpload import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn @@ -148,7 +148,7 @@ internal abstract class AbstractUser( ) }.recoverCatchingSuppressed { // try upload by http on provided servers - tryServers( + tryServersUpload( bot = bot, servers = resp.serverIp.zip(resp.serverPort), resourceSize = resource.size, diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 8ce574f5f..dd9ce345b 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -192,7 +192,7 @@ internal class GroupImpl( }.recoverCatchingSuppressed { when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()) { is PttStore.GroupPttUp.Response.RequireUpload -> { - tryServers( + tryServersUpload( bot, resp.uploadIpList.zip(resp.uploadPortList), resource.size, diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 1c51b9a29..1f26769a3 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -9,24 +9,111 @@ package net.mamoe.mirai.internal.message -import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey -import net.mamoe.mirai.message.data.AbstractServiceMessage -import net.mamoe.mirai.message.data.ServiceMessage -import net.mamoe.mirai.utils.safeCast +import io.ktor.client.request.* +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.internal.asQQAndroidBot +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.proto.LongMsg +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit +import net.mamoe.mirai.internal.network.protocol.packet.chat.MultiMsg +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.internal.utils.crypto.TEA +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.* // internal runtime value, not serializable internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) : - AbstractServiceMessage() { + AbstractServiceMessage(), RefinableMessage { override val serviceId: Int get() = 35 + override suspend fun refine(contact: Contact, context: MessageChain): Message { + val bot = contact.bot.asQQAndroidBot() + when (val resp = MultiMsg.ApplyDown(bot.client, 1, resId, 1).sendAndExpect(bot)) { + is MultiMsg.ApplyDown.Response.RequireDownload -> { + val http = Mirai.Http + val origin = resp.origin + + val data = if (origin.msgExternInfo?.channelType == 2) { + tryDownload( + bot = bot, + host = "https://ssl.htdata.qq.com", + port = 0, + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { host, port -> + http.get("$host${origin.thumbDownPara}:$port") + } + } else tryServersDownload( + bot = bot, + servers = origin.uint32DownIp.zip(origin.uint32DownPort), + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { ip, port -> + http.get("http://$ip${origin.thumbDownPara}:$port") + } + + val body = data.read { + check(readByte() == 40.toByte()) { + "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}" + } + val headLength = readInt() + val bodyLength = readInt() + discardExact(headLength) + readBytes(bodyLength) + } + + val decrypted = TEA.decrypt(body, origin.msgKey) + val longResp = + decrypted.loadAs(LongMsg.RspBody.serializer()) + + val down = longResp.msgDownRsp.single() + check(down.result == 0) { + "Message download failed, result=${down.result}, resId=${down.msgResid}, msgContent=${down.msgContent.toUHexString()}" + } + + val content = down.msgContent.ungzip() + val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) + + val source = context.source + return transmit.msg.toMessageChainNoSource(bot.id, contact.castOrNull()?.id ?: 0, source.kind) + } + MultiMsg.ApplyDown.Response.MessageTooLarge -> { + error("Message is too large and cannot download") + } + } + } + companion object Key : AbstractPolymorphicMessageKey(ServiceMessage, { it.safeCast() }) } // internal runtime value, not serializable -internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() { +internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage(), RefinableMessage { override val serviceId: Int get() = 35 + override suspend fun refine(contact: Contact, context: MessageChain): Message { + // val bot = contact.bot.asQQAndroidBot() + // TODO: 2021/2/2 Support forward message refinement + // https://github.com/mamoe/mirai/issues/623 + return this + } + companion object Key : AbstractPolymorphicMessageKey(ServiceMessage, { it.safeCast() }) +} + +internal interface RefinableMessage : SingleMessage { + + suspend fun refine( + contact: Contact, + context: MessageChain, + ): Message? } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index eab697c50..1b1e68f7d 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.message import kotlinx.io.core.discardExact import kotlinx.io.core.readUInt import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toVoice @@ -410,4 +411,21 @@ private object ReceiveMessageTransformer { format, kotlinx.io.core.String(downPara) ) -} \ No newline at end of file +} + +/** + * 解析 [ForwardMessageInternal], [LongMessageInternal] + */ +internal suspend fun MessageChain.refine(contact: Contact): MessageChain { + if (none { it is RefinableMessage }) return this + val builder = MessageChainBuilder(this.size) + for (singleMessage in this) { + if (singleMessage is RefinableMessage) { + val v = singleMessage.refine(contact, this) + if (v != null) builder.add(v) + } else { + builder.add(singleMessage) + } + } + return builder.build() +} diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index 799e74f98..68144652d 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -38,6 +38,8 @@ import kotlin.math.roundToInt import kotlin.time.measureTime internal object Highway { + + @Suppress("ArrayInDataClass") data class BdhUploadResponse( var extendInfo: ByteArray? = null, @@ -55,7 +57,7 @@ internal object Highway { ): BdhUploadResponse { val bdhSession = bot.client.bdhSession.await() // no need to care about timeout. proceed by bot init - return tryServers( + return tryServersUpload( bot = bot, servers = if (tryOnce) listOf(bdhSession.ssoAddresses.random()) else bdhSession.ssoAddresses, resourceSize = resource.size, @@ -113,7 +115,7 @@ internal enum class ChannelKind( override fun toString(): String = display } -internal suspend inline fun tryServers( +internal suspend inline fun tryServersUpload( bot: QQAndroidBot, servers: Collection>, resourceSize: Long, @@ -147,6 +149,59 @@ internal suspend inline fun tryServers( resp as R } +internal suspend inline fun tryServersDownload( + bot: QQAndroidBot, + servers: Collection>, + resourceKind: ResourceKind, + channelKind: ChannelKind, + crossinline implOnEachServer: suspend (ip: String, port: Int) -> R +) = servers.retryWithServers( + 5000, + onFail = { throw IllegalStateException("cannot download $resourceKind, failed on all servers.", it) } +) { ip, port -> + tryUploadImplEach(bot, channelKind, resourceKind, ip, port, implOnEachServer) +} + +internal suspend inline fun tryDownload( + bot: QQAndroidBot, + host: String, + port: Int, + resourceKind: ResourceKind, + channelKind: ChannelKind, + crossinline implOnEachServer: suspend (ip: String, port: Int) -> R +) = runCatching { + tryUploadImplEach(bot, channelKind, resourceKind, host, port, implOnEachServer) +}.getOrElse { throw IllegalStateException("cannot upload $resourceKind, failed on all servers.", it) } + +private suspend inline fun tryUploadImplEach( + bot: QQAndroidBot, + channelKind: ChannelKind, + resourceKind: ResourceKind, + host: String, + port: Int, + crossinline implOnEachServer: suspend (ip: String, port: Int) -> R +): R { + bot.network.logger.verbose { + "[${channelKind}] Downloading $resourceKind to ${host}:$port" + } + + var resp: R? = null + runCatching { + resp = implOnEachServer(host, port) + }.onFailure { + bot.network.logger.verbose { + "[${channelKind}] Downloading $resourceKind to ${host}:$port failed: $it" + } + throw it + } + + bot.network.logger.verbose { + "[${channelKind}] Downloading $resourceKind: succeed" + } + + return resp as R +} + internal suspend fun ChunkedFlowSession.sendSequentially( socket: PlatformSocket, respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {} diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgTransmit.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgTransmit.kt index 1a4fd8a61..5410cf75a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgTransmit.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgTransmit.kt @@ -16,19 +16,19 @@ import net.mamoe.mirai.internal.utils.io.ProtoBuf internal class MsgTransmit : ProtoBuf { @Serializable -internal class PbMultiMsgItem( + internal class PbMultiMsgItem( @ProtoNumber(1) @JvmField val fileName: String = "", @ProtoNumber(2) @JvmField val buffer: ByteArray = EMPTY_BYTE_ARRAY ) : ProtoBuf @Serializable -internal class PbMultiMsgNew( + internal class PbMultiMsgNew( @ProtoNumber(1) @JvmField val msg: List = emptyList() ) : ProtoBuf @Serializable -internal class PbMultiMsgTransmit( + internal class PbMultiMsgTransmit( @ProtoNumber(1) @JvmField val msg: List = emptyList(), - @ProtoNumber(2) @JvmField val pbItemList: List = emptyList() + @ProtoNumber(2) @JvmField val pbItemList: List = emptyList() ) : ProtoBuf } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt index 378c87995..24c32811c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MultiMsg.kt @@ -31,11 +31,11 @@ internal class MultiMsg : ProtoBuf { @Serializable internal class MultiMsgApplyDownRsp( @ProtoNumber(1) @JvmField val result: Int = 0, - @ProtoNumber(2) @JvmField val thumbDownPara: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(2) @JvmField val thumbDownPara: String = "", @ProtoNumber(3) @JvmField val msgKey: ByteArray = EMPTY_BYTE_ARRAY, @ProtoNumber(4) @JvmField val uint32DownIp: List = emptyList(), @ProtoNumber(5) @JvmField val uint32DownPort: List = emptyList(), - @ProtoNumber(6) @JvmField val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(6) @JvmField val msgResid: String = "", @ProtoNumber(7) @JvmField val msgExternInfo: ExternMsg? = null, @ProtoNumber(8) @JvmField val bytesDownIpV6: List = emptyList(), @ProtoNumber(9) @JvmField val uint32DownV6Port: List = emptyList() diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt index 6f62fa84c..3bd6639cd 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.SendMessageHandler +import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.toRichTextElems import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient @@ -152,8 +153,13 @@ internal class MultiMsg { } } - object ApplyDown: OutgoingPacketFactory("MultiMsg.ApplyDown") { + object ApplyDown : OutgoingPacketFactory("MultiMsg.ApplyDown") { sealed class Response : Packet { + class RequireDownload( + val origin: MultiMsg.MultiMsgApplyDownRsp + ) : Response() { + override fun toString(): String = "MultiMsg.ApplyDown.Response" + } object MessageTooLarge : Response() } @@ -171,7 +177,7 @@ internal class MultiMsg { buildVer = "8.2.0.1296", multimsgApplydownReq = listOf( MultiMsg.MultiMsgApplyDownReq( - msgResid = resId, + msgResid = resId, msgType = msgType, ) ), @@ -185,7 +191,18 @@ internal class MultiMsg { } override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { - return Response.MessageTooLarge + val body = readProtoBuf(MultiMsg.RspBody.serializer()) + val response = body.multimsgApplydownRsp.first() + return when (response.result) { + 0 -> Response.RequireDownload(response) + 193 -> Response.MessageTooLarge + //1 -> Response.OK(resId = response.msgResid) + else -> throw contextualBugReportException( + "MultiMsg.ApplyDown", + response._miraiContentToString(), + additional = "Decode failure result=${response.result}" + ) + } } } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt index e82339dff..9c2440a7f 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt @@ -30,6 +30,7 @@ import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.message.OnlineMessageSourceFromFriendImpl +import net.mamoe.mirai.internal.message.refine import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.MultiPacket import net.mamoe.mirai.internal.network.Packet @@ -49,6 +50,8 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.message.data.MessageSourceKind +import net.mamoe.mirai.message.data.MessageSourceKind.STRANGER +import net.mamoe.mirai.message.data.MessageSourceKind.TEMP import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.buildMessageChain import net.mamoe.mirai.utils.* @@ -389,29 +392,22 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean friend.checkIsFriendImpl() friend.lastMessageSequence.loop { //我也不知道为什么要这样写,但它就是能跑 - return if (friend.lastMessageSequence.value != msgHead.msgSeq && friend.lastMessageSequence.compareAndSet( - it, - msgHead.msgSeq - ) && contentHead?.autoReply != 1 + return if (friend.lastMessageSequence.value != msgHead.msgSeq + && friend.lastMessageSequence.compareAndSet(it, msgHead.msgSeq) + && contentHead?.autoReply != 1 ) { val msgs = friend.friendPkgMsgParsingCache.tryMerge(this) if (msgs.isNotEmpty()) { if (fromSync) { FriendMessageSyncEvent( friend, - msgs.toMessageChainOnline( - bot = bot, groupIdOrZero = 0, - messageSourceKind = MessageSourceKind.FRIEND - ), + msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND).refine(friend), msgHead.msgTime ) } else { FriendMessageEvent( friend, - msgs.toMessageChainOnline( - bot = bot, groupIdOrZero = 0, - MessageSourceKind.FRIEND - ), + msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND).refine(friend), msgHead.msgTime ) } @@ -430,13 +426,13 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean if (fromSync) { StrangerMessageSyncEvent( stranger, - listOf(this).toMessageChainOnline(bot, groupIdOrZero = 0, MessageSourceKind.STRANGER), + listOf(this).toMessageChainOnline(bot, 0, STRANGER).refine(stranger), msgHead.msgTime ) } else { StrangerMessageEvent( stranger, - listOf(this).toMessageChainOnline(bot, groupIdOrZero = 0, MessageSourceKind.STRANGER), + listOf(this).toMessageChainOnline(bot, 0, STRANGER).refine(stranger), msgHead.msgTime ) } @@ -510,13 +506,13 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean return if (fromSync) { GroupTempMessageSyncEvent( member, - listOf(this).toMessageChainOnline(bot, 0, MessageSourceKind.TEMP), + listOf(this).toMessageChainOnline(bot, 0, TEMP).refine(member), msgHead.msgTime ) } else { GroupTempMessageEvent( member, - listOf(this).toMessageChainOnline(bot, 0, MessageSourceKind.TEMP), + listOf(this).toMessageChainOnline(bot, 0, TEMP).refine(member), msgHead.msgTime ) } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt index 982de268b..56809219d 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt @@ -23,6 +23,7 @@ import net.mamoe.mirai.event.events.GroupMessageSyncEvent import net.mamoe.mirai.event.events.MemberCardChangeEvent import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.* +import net.mamoe.mirai.internal.message.refine import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody @@ -33,7 +34,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf -import net.mamoe.mirai.message.data.MessageSourceKind +import net.mamoe.mirai.message.data.MessageSourceKind.GROUP import net.mamoe.mirai.utils.* /** @@ -119,7 +120,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin if (isFromSelfAccount) { return GroupMessageSyncEvent( - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP,), + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP).refine(group), time = msgHead.msgTime, group = group, sender = sender, @@ -132,7 +133,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin return GroupMessageEvent( senderName = name, sender = sender, - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP).refine(group), permission = findMemberPermission(extraInfo?.flags ?: 0, sender, bot), time = msgHead.msgTime ) From 1e95c43ff64731e8468be767004213547d0d3fb9 Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 15:13:50 +0800 Subject: [PATCH 05/13] Add LongMessageOrigin and extract public api IMirai.downloadLongMessage --- .../api/binary-compatibility-validator.api | 19 +++++ .../src/commonMain/kotlin/IMirai.kt | 8 +++ .../kotlin/message/data/LongMessageOrigin.kt | 36 ++++++++++ mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 64 ++++++++++++++++- .../kotlin/message/LongMessageInternal.kt | 70 +------------------ 5 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index f53f76e59..8d36d5dac 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -91,6 +91,8 @@ public abstract interface class net/mamoe/mirai/IMirai : net/mamoe/mirai/LowLeve public abstract fun createImage (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public synthetic fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)Lkotlin/Unit; public fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)V + public fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/MessageChain; + public abstract fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getBotFactory ()Lnet/mamoe/mirai/BotFactory; public abstract fun getFileCacheStrategy ()Lnet/mamoe/mirai/utils/FileCacheStrategy; public fun getGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)Lnet/mamoe/mirai/data/GroupAnnouncement; @@ -4364,6 +4366,23 @@ public final class net/mamoe/mirai/message/data/LightApp$Key : net/mamoe/mirai/m public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class net/mamoe/mirai/message/data/LongMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { + public static final field Key Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; + public static synthetic fun copy$default (Lnet/mamoe/mirai/message/data/LongMessageOrigin;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; + public fun equals (Ljava/lang/Object;)Z + public fun getKey ()Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; + public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public final fun getResourceId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/LongMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey { +} + public abstract interface class net/mamoe/mirai/message/data/MarketFace : net/mamoe/mirai/message/data/HummerMessage { public static final field Key Lnet/mamoe/mirai/message/data/MarketFace$Key; public static final field SERIAL_NAME Ljava/lang/String; diff --git a/mirai-core-api/src/commonMain/kotlin/IMirai.kt b/mirai-core-api/src/commonMain/kotlin/IMirai.kt index d694a14cf..0a2764088 100644 --- a/mirai-core-api/src/commonMain/kotlin/IMirai.kt +++ b/mirai-core-api/src/commonMain/kotlin/IMirai.kt @@ -170,6 +170,14 @@ public interface IMirai : LowLevelApiAccessor { originalMessage: MessageChain ): OfflineMessageSource + /** + * @since 2.3 + */ + @JvmBlockingBridge + public suspend fun downloadLongMessage( + bot: Bot, + resourceId: String, + ): MessageChain /** * 通过好友验证 diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt b/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt new file mode 100644 index 000000000..ddc006c1e --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt @@ -0,0 +1,36 @@ +/* + * 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 + */ + +package net.mamoe.mirai.message.data + +import net.mamoe.mirai.IMirai +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.safeCast + +/** + * 标识一个长消息. + * + * + * 消息过长后会通过特殊的通道上传和下载, 每条消息都会获得一个 resourceId. + * + * 可以通过 resourceId 下载消息 [IMirai.downloadLongMessage]. + * 但不保证 resourceId 一直有效. + * + * @since 2.3 + */ +@MiraiExperimentalApi +public data class LongMessageOrigin( + val resourceId: String +) : MessageMetadata, ConstrainSingle { + override val key: Key get() = Key + + override fun toString(): String = "" + + public companion object Key : AbstractMessageKey({ it.safeCast() }) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index f86bc7576..f428c76eb 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -16,6 +16,8 @@ import io.ktor.client.request.* import io.ktor.client.request.forms.* import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.withContext +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes import kotlinx.serialization.json.* import net.mamoe.mirai.* import net.mamoe.mirai.contact.* @@ -24,15 +26,18 @@ import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.message.* -import net.mamoe.mirai.internal.network.highway.Highway -import net.mamoe.mirai.internal.network.highway.ResourceKind +import net.mamoe.mirai.internal.network.highway.* import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg +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.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect import net.mamoe.mirai.internal.network.protocol.packet.summarycard.SummaryCard +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 @@ -959,4 +964,59 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { kind, ids, botId, time, fromId, targetId, originalMessage, internalIds ) + override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain { + bot.asQQAndroidBot() + when (val resp = MultiMsg.ApplyDown(bot.client, 1, resourceId, 1).sendAndExpect(bot)) { + is MultiMsg.ApplyDown.Response.RequireDownload -> { + val http = Mirai.Http + val origin = resp.origin + + val data = if (origin.msgExternInfo?.channelType == 2) { + tryDownload( + bot = bot, + host = "https://ssl.htdata.qq.com", + port = 0, + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { host, port -> + http.get("$host${origin.thumbDownPara}:$port") + } + } else tryServersDownload( + bot = bot, + servers = origin.uint32DownIp.zip(origin.uint32DownPort), + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { ip, port -> + http.get("http://$ip${origin.thumbDownPara}:$port") + } + + val body = data.read { + check(readByte() == 40.toByte()) { + "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}" + } + val headLength = readInt() + val bodyLength = readInt() + discardExact(headLength) + readBytes(bodyLength) + } + + val decrypted = TEA.decrypt(body, origin.msgKey) + val longResp = + decrypted.loadAs(LongMsg.RspBody.serializer()) + + val down = longResp.msgDownRsp.single() + check(down.result == 0) { + "Message download failed, result=${down.result}, resId=${down.msgResid}, msgContent=${down.msgContent.toUHexString()}" + } + + val content = down.msgContent.ungzip() + val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) + + return transmit.msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP) + } + MultiMsg.ApplyDown.Response.MessageTooLarge -> { + error("Message is too large and cannot download") + } + } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 1f26769a3..3c6cc9b40 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -9,25 +9,11 @@ package net.mamoe.mirai.internal.message -import io.ktor.client.request.* -import kotlinx.io.core.discardExact -import kotlinx.io.core.readBytes import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.asQQAndroidBot -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.proto.LongMsg -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit -import net.mamoe.mirai.internal.network.protocol.packet.chat.MultiMsg -import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect -import net.mamoe.mirai.internal.utils.crypto.TEA -import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.safeCast // internal runtime value, not serializable internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) : @@ -36,59 +22,9 @@ internal data class LongMessageInternal internal constructor(override val conten override suspend fun refine(contact: Contact, context: MessageChain): Message { val bot = contact.bot.asQQAndroidBot() - when (val resp = MultiMsg.ApplyDown(bot.client, 1, resId, 1).sendAndExpect(bot)) { - is MultiMsg.ApplyDown.Response.RequireDownload -> { - val http = Mirai.Http - val origin = resp.origin + val long = Mirai.downloadLongMessage(bot, resId) - val data = if (origin.msgExternInfo?.channelType == 2) { - tryDownload( - bot = bot, - host = "https://ssl.htdata.qq.com", - port = 0, - resourceKind = ResourceKind.LONG_MESSAGE, - channelKind = ChannelKind.HTTP - ) { host, port -> - http.get("$host${origin.thumbDownPara}:$port") - } - } else tryServersDownload( - bot = bot, - servers = origin.uint32DownIp.zip(origin.uint32DownPort), - resourceKind = ResourceKind.LONG_MESSAGE, - channelKind = ChannelKind.HTTP - ) { ip, port -> - http.get("http://$ip${origin.thumbDownPara}:$port") - } - - val body = data.read { - check(readByte() == 40.toByte()) { - "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}" - } - val headLength = readInt() - val bodyLength = readInt() - discardExact(headLength) - readBytes(bodyLength) - } - - val decrypted = TEA.decrypt(body, origin.msgKey) - val longResp = - decrypted.loadAs(LongMsg.RspBody.serializer()) - - val down = longResp.msgDownRsp.single() - check(down.result == 0) { - "Message download failed, result=${down.result}, resId=${down.msgResid}, msgContent=${down.msgContent.toUHexString()}" - } - - val content = down.msgContent.ungzip() - val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) - - val source = context.source - return transmit.msg.toMessageChainNoSource(bot.id, contact.castOrNull()?.id ?: 0, source.kind) - } - MultiMsg.ApplyDown.Response.MessageTooLarge -> { - error("Message is too large and cannot download") - } - } + return LongMessageOrigin(resId) + long } companion object Key : From 82b355a6c62017f6e50e9b3c5c2faebf2923265b Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 17:49:59 +0800 Subject: [PATCH 06/13] Fix download url --- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index f428c76eb..36e959254 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -979,7 +979,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { resourceKind = ResourceKind.LONG_MESSAGE, channelKind = ChannelKind.HTTP ) { host, port -> - http.get("$host${origin.thumbDownPara}:$port") + http.get("$host:$port${origin.thumbDownPara}") } } else tryServersDownload( bot = bot, @@ -987,7 +987,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { resourceKind = ResourceKind.LONG_MESSAGE, channelKind = ChannelKind.HTTP ) { ip, port -> - http.get("http://$ip${origin.thumbDownPara}:$port") + http.get("http://$ip:$port${origin.thumbDownPara}") } val body = data.read { From cd146e597664f6207b0109808fd024af54cadd42 Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 18:07:18 +0800 Subject: [PATCH 07/13] Fix forward and long detect --- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 2 +- .../src/commonMain/kotlin/QQAndroidBot.kt | 2 +- .../kotlin/message/LongMessageInternal.kt | 2 +- .../kotlin/message/ReceiveMessageHandler.kt | 25 +++++++++++-------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 36e959254..6b6aac805 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -966,7 +966,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain { bot.asQQAndroidBot() - when (val resp = MultiMsg.ApplyDown(bot.client, 1, resourceId, 1).sendAndExpect(bot)) { + when (val resp = MultiMsg.ApplyDown(bot.client, 2, resourceId, 1).sendAndExpect(bot)) { is MultiMsg.ApplyDown.Response.RequireDownload -> { val http = Mirai.Http val origin = resp.origin diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index ec53ede39..3f6d1012a 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -202,5 +202,5 @@ internal fun RichMessage.Key.forwardMessage( """.trimIndent().replace("\n", " ") - return ForwardMessageInternal(template) + return ForwardMessageInternal(template, resId) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 3c6cc9b40..183f59d16 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -32,7 +32,7 @@ internal data class LongMessageInternal internal constructor(override val conten } // internal runtime value, not serializable -internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage(), RefinableMessage { +internal data class ForwardMessageInternal(override val content: String, val resId: String) : AbstractServiceMessage(), RefinableMessage { override val serviceId: Int get() = 35 override suspend fun refine(contact: Contact, context: MessageChain): Message { diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index 1b1e68f7d..ca4dc75e3 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -118,12 +118,10 @@ private object ReceiveMessageTransformer { builder: MessageChainBuilder ) { // (this._miraiContentToString().soutv()) - val generalFlags = elements.find { it.generalFlags != null }?.generalFlags - for (element in elements) { transformElement(element, groupIdOrZero, messageSourceKind, botId, builder) when { - element.richMsg != null -> decodeRichMessage(generalFlags, element.richMsg, builder) + element.richMsg != null -> decodeRichMessage(element.richMsg, builder) } } } @@ -359,7 +357,6 @@ private object ReceiveMessageTransformer { } private fun decodeRichMessage( - generalFlags: ImMsgBody.GeneralFlags?, richMsg: ImMsgBody.RichMsg, builder: MessageChainBuilder ) { @@ -388,13 +385,21 @@ private object ReceiveMessageTransformer { * [LongMessageInternal], [ForwardMessage] */ 35 -> { - val resId = generalFlags?.longTextResid - - if (resId != null) { - builder.add(LongMessageInternal(content, resId)) - } else { - builder.add(ForwardMessageInternal(content)) + fun findStringProperty(name: String): String { + return content.substringAfter("$name=\"", "").substringBefore("\"", "") } + + val resId = findStringProperty("m_resid") + + val msg = when(findStringProperty("multiMsgFlag").toIntOrNull()) { + 1 -> LongMessageInternal(content, resId) + 0 -> ForwardMessageInternal(content, resId) + else -> { + SimpleServiceMessage(35, content) + } + } + + builder.add(msg) } // 104 新群员入群的消息 From f2fe94ed67dfcd911770e61f66af71634be172ca Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 18:09:37 +0800 Subject: [PATCH 08/13] Rename LongMessageOrigin to RichMessageOrigin --- .../data/{LongMessageOrigin.kt => RichMessageOrigin.kt} | 4 ++-- .../src/commonMain/kotlin/message/LongMessageInternal.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename mirai-core-api/src/commonMain/kotlin/message/data/{LongMessageOrigin.kt => RichMessageOrigin.kt} (90%) diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt b/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt similarity index 90% rename from mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt rename to mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt index ddc006c1e..f938f8c4c 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt @@ -25,12 +25,12 @@ import net.mamoe.mirai.utils.safeCast * @since 2.3 */ @MiraiExperimentalApi -public data class LongMessageOrigin( +public data class RichMessageOrigin( val resourceId: String ) : MessageMetadata, ConstrainSingle { override val key: Key get() = Key override fun toString(): String = "" - public companion object Key : AbstractMessageKey({ it.safeCast() }) + public companion object Key : AbstractMessageKey({ it.safeCast() }) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 183f59d16..fea6bcaa1 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -24,7 +24,7 @@ internal data class LongMessageInternal internal constructor(override val conten val bot = contact.bot.asQQAndroidBot() val long = Mirai.downloadLongMessage(bot, resId) - return LongMessageOrigin(resId) + long + return RichMessageOrigin(resId) + long } companion object Key : From dd08c8815b962d5ee4860184255d109f2805d0c7 Mon Sep 17 00:00:00 2001 From: Him188 Date: Tue, 2 Feb 2021 18:15:42 +0800 Subject: [PATCH 09/13] apiDump --- .../api/binary-compatibility-validator.api | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 8d36d5dac..f3fe87604 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -4366,23 +4366,6 @@ public final class net/mamoe/mirai/message/data/LightApp$Key : net/mamoe/mirai/m public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class net/mamoe/mirai/message/data/LongMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { - public static final field Key Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; - public static synthetic fun copy$default (Lnet/mamoe/mirai/message/data/LongMessageOrigin;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; - public fun equals (Ljava/lang/Object;)Z - public fun getKey ()Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; - public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; - public final fun getResourceId ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class net/mamoe/mirai/message/data/LongMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey { -} - public abstract interface class net/mamoe/mirai/message/data/MarketFace : net/mamoe/mirai/message/data/HummerMessage { public static final field Key Lnet/mamoe/mirai/message/data/MarketFace$Key; public static final field SERIAL_NAME Ljava/lang/String; @@ -5090,6 +5073,23 @@ public final class net/mamoe/mirai/message/data/RichMessage$Key : net/mamoe/mira public static synthetic fun share$default (Lnet/mamoe/mirai/message/data/RichMessage$Key;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/ServiceMessage; } +public final class net/mamoe/mirai/message/data/RichMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { + public static final field Key Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RichMessageOrigin; + public static synthetic fun copy$default (Lnet/mamoe/mirai/message/data/RichMessageOrigin;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/RichMessageOrigin; + public fun equals (Ljava/lang/Object;)Z + public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public fun getKey ()Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key; + public final fun getResourceId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/RichMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey { +} + public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage { public static final field Key Lnet/mamoe/mirai/message/data/ServiceMessage$Key; public fun appendMiraiCodeTo (Ljava/lang/StringBuilder;)V From d7272e7e9ad9b107e3119bdecdc293eeadc35eb0 Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Tue, 2 Feb 2021 20:12:37 +0800 Subject: [PATCH 10/13] Fix resource download: - fix download/upload logging - add retry fro single server download --- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 9 +++++---- .../commonMain/kotlin/network/highway/Highway.kt | 16 +++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 6b6aac805..9ea0a8623 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -971,15 +971,16 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { val http = Mirai.Http val origin = resp.origin - val data = if (origin.msgExternInfo?.channelType == 2) { + val data: ByteArray = if (origin.msgExternInfo?.channelType == 2) { tryDownload( bot = bot, host = "https://ssl.htdata.qq.com", - port = 0, + port = 443, + times = 3, resourceKind = ResourceKind.LONG_MESSAGE, channelKind = ChannelKind.HTTP - ) { host, port -> - http.get("$host:$port${origin.thumbDownPara}") + ) { host, _ -> + http.get("$host${origin.thumbDownPara}") } } else tryServersDownload( bot = bot, diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index 68144652d..0b4078e57 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -159,21 +159,23 @@ internal suspend inline fun tryServersDownload( 5000, onFail = { throw IllegalStateException("cannot download $resourceKind, failed on all servers.", it) } ) { ip, port -> - tryUploadImplEach(bot, channelKind, resourceKind, ip, port, implOnEachServer) + tryDownloadImplEach(bot, channelKind, resourceKind, ip, port, implOnEachServer) } internal suspend inline fun tryDownload( bot: QQAndroidBot, host: String, port: Int, + times: Int = 1, resourceKind: ResourceKind, channelKind: ChannelKind, crossinline implOnEachServer: suspend (ip: String, port: Int) -> R -) = runCatching { - tryUploadImplEach(bot, channelKind, resourceKind, host, port, implOnEachServer) -}.getOrElse { throw IllegalStateException("cannot upload $resourceKind, failed on all servers.", it) } +) = retryCatching(times) { + tryDownloadImplEach(bot, channelKind, resourceKind, host, port, implOnEachServer) +}.getOrElse { throw IllegalStateException("Cannot download $resourceKind", it) } -private suspend inline fun tryUploadImplEach( + +private suspend inline fun tryDownloadImplEach( bot: QQAndroidBot, channelKind: ChannelKind, resourceKind: ResourceKind, @@ -182,7 +184,7 @@ private suspend inline fun tryUploadImplEach( crossinline implOnEachServer: suspend (ip: String, port: Int) -> R ): R { bot.network.logger.verbose { - "[${channelKind}] Downloading $resourceKind to ${host}:$port" + "[${channelKind}] Downloading $resourceKind from ${host}:$port" } var resp: R? = null @@ -190,7 +192,7 @@ private suspend inline fun tryUploadImplEach( resp = implOnEachServer(host, port) }.onFailure { bot.network.logger.verbose { - "[${channelKind}] Downloading $resourceKind to ${host}:$port failed: $it" + "[${channelKind}] Downloading $resourceKind from ${host}:$port failed: $it" } throw it } From b659d55fecdce505ca99f93035dbeaa9a84f7afe Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Tue, 2 Feb 2021 20:14:40 +0800 Subject: [PATCH 11/13] Support forward message refinement, close #623 --- .../api/binary-compatibility-validator.api | 2 + .../src/commonMain/kotlin/IMirai.kt | 9 ++++ mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 30 +++++++++-- .../kotlin/message/LongMessageInternal.kt | 51 ++++++++++++++++--- .../kotlin/message/ReceiveMessageHandler.kt | 11 +++- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index f3fe87604..42d5dca55 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -91,6 +91,8 @@ public abstract interface class net/mamoe/mirai/IMirai : net/mamoe/mirai/LowLeve public abstract fun createImage (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public synthetic fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)Lkotlin/Unit; public fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)V + public fun downloadForwardMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;)Ljava/util/List; + public abstract fun downloadForwardMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/MessageChain; public abstract fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getBotFactory ()Lnet/mamoe/mirai/BotFactory; diff --git a/mirai-core-api/src/commonMain/kotlin/IMirai.kt b/mirai-core-api/src/commonMain/kotlin/IMirai.kt index 0a2764088..260e1350f 100644 --- a/mirai-core-api/src/commonMain/kotlin/IMirai.kt +++ b/mirai-core-api/src/commonMain/kotlin/IMirai.kt @@ -179,6 +179,15 @@ public interface IMirai : LowLevelApiAccessor { resourceId: String, ): MessageChain + /** + * @since 2.3 + */ + @JvmBlockingBridge + public suspend fun downloadForwardMessage( + bot: Bot, + resourceId: String, + ): List + /** * 通过好友验证 * diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 9ea0a8623..fd716b7cc 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -965,6 +965,28 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { ) override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain { + return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.LONG_MESSAGE).msg + .toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP) + } + + override suspend fun downloadForwardMessage(bot: Bot, resourceId: String): List { + return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.FORWARD_MESSAGE).msg.map { msg -> + ForwardMessage.Node( + senderId = msg.msgHead.fromUin, + time = msg.msgHead.msgTime, + senderName = msg.msgHead.groupInfo?.groupCard + ?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() } + ?: msg.msgHead.fromUin.toString(), + messageChain = listOf(msg).toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP) + ) + } + } + + private suspend fun downloadMultiMsgTransmit( + bot: Bot, + resourceId: String, + resourceKind: ResourceKind, + ): MsgTransmit.PbMultiMsgTransmit { bot.asQQAndroidBot() when (val resp = MultiMsg.ApplyDown(bot.client, 2, resourceId, 1).sendAndExpect(bot)) { is MultiMsg.ApplyDown.Response.RequireDownload -> { @@ -977,7 +999,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { host = "https://ssl.htdata.qq.com", port = 443, times = 3, - resourceKind = ResourceKind.LONG_MESSAGE, + resourceKind = resourceKind, channelKind = ChannelKind.HTTP ) { host, _ -> http.get("$host${origin.thumbDownPara}") @@ -985,7 +1007,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { } else tryServersDownload( bot = bot, servers = origin.uint32DownIp.zip(origin.uint32DownPort), - resourceKind = ResourceKind.LONG_MESSAGE, + resourceKind = resourceKind, channelKind = ChannelKind.HTTP ) { ip, port -> http.get("http://$ip:$port${origin.thumbDownPara}") @@ -1011,9 +1033,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { } val content = down.msgContent.ungzip() - val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) - - return transmit.msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP) + return content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) } MultiMsg.ApplyDown.Response.MessageTooLarge -> { error("Message is too large and cannot download") diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index fea6bcaa1..11217fb02 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -32,18 +32,57 @@ internal data class LongMessageInternal internal constructor(override val conten } // internal runtime value, not serializable -internal data class ForwardMessageInternal(override val content: String, val resId: String) : AbstractServiceMessage(), RefinableMessage { +@Suppress("RegExpRedundantEscape", "UnnecessaryVariable") +internal data class ForwardMessageInternal(override val content: String, val resId: String) : AbstractServiceMessage(), + RefinableMessage { override val serviceId: Int get() = 35 override suspend fun refine(contact: Contact, context: MessageChain): Message { - // val bot = contact.bot.asQQAndroidBot() - // TODO: 2021/2/2 Support forward message refinement - // https://github.com/mamoe/mirai/issues/623 - return this + val bot = contact.bot.asQQAndroidBot() + + val msgXml = content.substringAfter("(ServiceMessage, { it.safeCast() }) + AbstractPolymorphicMessageKey(ServiceMessage, { it.safeCast() }) { + + val SUMMARY_REGEX = """\(.*?)\<\/summary\>""".toRegex() + + @Suppress("SpellCheckingInspection") + val TITLE_REGEX = """\([\u0000-\uFFFF]*?)\<\/title\>""".toRegex() + + + fun String.findField(type: String): String { + return substringAfter("$type=\"", "") + .substringBefore("\"", "") + } + } } internal interface RefinableMessage : SingleMessage { diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index ca4dc75e3..fe9e1a8fd 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -391,11 +391,18 @@ private object ReceiveMessageTransformer { val resId = findStringProperty("m_resid") - val msg = when(findStringProperty("multiMsgFlag").toIntOrNull()) { + val msg = if (resId.isEmpty()) { + SimpleServiceMessage(35, content) + } else when (findStringProperty("multiMsgFlag").toIntOrNull()) { 1 -> LongMessageInternal(content, resId) 0 -> ForwardMessageInternal(content, resId) else -> { - SimpleServiceMessage(35, content) + // from PC QQ + if (findStringProperty("action") == "viewMultiMsg") { + ForwardMessageInternal(content, resId) + } else { + SimpleServiceMessage(35, content) + } } } From e381a2b33f07587e95e9c94d4bdde4b6df331281 Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 3 Feb 2021 08:28:02 +0800 Subject: [PATCH 12/13] Ignore unrecognized message types on messageToElems transformation --- mirai-core/src/commonMain/kotlin/message/messageToElems.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mirai-core/src/commonMain/kotlin/message/messageToElems.kt b/mirai-core/src/commonMain/kotlin/message/messageToElems.kt index c5ada71f9..6b194c26c 100644 --- a/mirai-core/src/commonMain/kotlin/message/messageToElems.kt +++ b/mirai-core/src/commonMain/kotlin/message/messageToElems.kt @@ -238,7 +238,10 @@ internal fun MessageChain.toRichTextElems( is InternalFlagOnlyMessage, is ShowImageFlag -> { // ignore } - else -> error("unsupported message type: ${currentMessage::class.simpleName}") + else -> { + // unrecognized types are ignored + // error("unsupported message type: ${currentMessage::class.simpleName}") + } } } this.forEach(::transformOneMessage) From 54050523c37591b7156f322b1b98eba1305f1c11 Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 3 Feb 2021 08:42:02 +0800 Subject: [PATCH 13/13] Generalize RichMessageOrigin, add origin and kind. Add RichMessageKind. #950 --- .../api/binary-compatibility-validator.api | 31 ++++++-- .../kotlin/message/data/RichMessageOrigin.kt | 72 ++++++++++++++++--- .../kotlin/message/LongMessageInternal.kt | 4 +- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 42d5dca55..d98a3a9c6 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -5075,21 +5075,40 @@ public final class net/mamoe/mirai/message/data/RichMessage$Key : net/mamoe/mira public static synthetic fun share$default (Lnet/mamoe/mirai/message/data/RichMessage$Key;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/ServiceMessage; } +public final class net/mamoe/mirai/message/data/RichMessageKind : java/lang/Enum { + public static final field FORWARD Lnet/mamoe/mirai/message/data/RichMessageKind; + public static final field LONG Lnet/mamoe/mirai/message/data/RichMessageKind; + public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RichMessageKind; + public static fun values ()[Lnet/mamoe/mirai/message/data/RichMessageKind; +} + public final class net/mamoe/mirai/message/data/RichMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { public static final field Key Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RichMessageOrigin; - public static synthetic fun copy$default (Lnet/mamoe/mirai/message/data/RichMessageOrigin;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/RichMessageOrigin; - public fun equals (Ljava/lang/Object;)Z + public synthetic fun (ILnet/mamoe/mirai/message/data/RichMessage;Ljava/lang/String;Lnet/mamoe/mirai/message/data/RichMessageKind;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Lnet/mamoe/mirai/message/data/RichMessage;Ljava/lang/String;Lnet/mamoe/mirai/message/data/RichMessageKind;)V + public fun contentToString ()Ljava/lang/String; public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; public fun getKey ()Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key; + public final fun getKind ()Lnet/mamoe/mirai/message/data/RichMessageKind; + public final fun getOrigin ()Lnet/mamoe/mirai/message/data/RichMessage; public final fun getResourceId ()Ljava/lang/String; - public fun hashCode ()I public fun toString ()Ljava/lang/String; + public static final fun write$Self (Lnet/mamoe/mirai/message/data/RichMessageOrigin;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public final class net/mamoe/mirai/message/data/RichMessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lnet/mamoe/mirai/message/data/RichMessageOrigin$$serializer; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RichMessageOrigin; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/RichMessageOrigin;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } public final class net/mamoe/mirai/message/data/RichMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey { + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage { diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt b/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt index f938f8c4c..89fae1787 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/RichMessageOrigin.kt @@ -7,30 +7,86 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("MemberVisibilityCanBePrivate", "unused") + package net.mamoe.mirai.message.data +import kotlinx.serialization.Polymorphic +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import net.mamoe.mirai.IMirai import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.safeCast /** - * 标识一个长消息. + * 标识来源 [RichMessage], 存在于接收的 [MessageChain] 中. 在发送消息时会被忽略. * + * 一些 [RichMessage] 会被 mirai 解析成特定的更易使用的类型, 如: + * - 长消息会被协议内部转化为 [ServiceMessage] `serviceId=35` 通过独立通道上传和下载并获得一个 [resourceId]. mirai 会自动下载长消息并把他们解析为 [MessageChain]. + * - 合并转发也使用长消息通道传输, 拥有 [resourceId], mirai 解析为 [ForwardMessage] + * - [MusicShare] 也有特殊通道上传, 但会作为 [LightApp] 接收. * - * 消息过长后会通过特殊的通道上传和下载, 每条消息都会获得一个 resourceId. + * 这些经过转换的类型的来源 [RichMessage] 会被包装为 [RichMessageOrigin] 并加入消息链中. * - * 可以通过 resourceId 下载消息 [IMirai.downloadLongMessage]. - * 但不保证 resourceId 一直有效. + * 如一条被 mirai 解析的长消息的消息链组成为, 第一个元素为 [MessageSource], 第二个元素为 [RichMessageOrigin], 随后为长消息内容. + * + * 又如一条被 mirai 解析的 [MusicShare] 的消息链组成为, 第一个元素为 [MessageSource], 第二个元素为 [RichMessageOrigin], 第三个元素为 [MusicShare]. + * + * @suppress **注意**: 这是实验性 API: 类名, 类的类型, 构造, 属性等所有 API 均不稳定. 可能会在未来任意时刻变更. * * @since 2.3 */ -@MiraiExperimentalApi -public data class RichMessageOrigin( - val resourceId: String +@Serializable +@SerialName("RichMessageOrigin") +@MiraiExperimentalApi("RichMessageOrigin 不稳定") +public class RichMessageOrigin( + /** + * 原 [RichMessage]. + */ + public val origin: @Polymorphic RichMessage, + /** + * 如果来自长消息或转发消息, 则会有 [resourceId], 否则为 `null`. + * + * - 下载长消息 [IMirai.downloadLongMessage] + * - 下载合并转发消息 [IMirai.downloadForwardMessage] + */ + public val resourceId: String?, + /** + * 来源类型 + */ + public val kind: RichMessageKind, ) : MessageMetadata, ConstrainSingle { override val key: Key get() = Key - override fun toString(): String = "" + override fun toString(): String { + val resourceId = resourceId + return if (resourceId == null) "[mirai:origin:$kind]" + else "[mirai:origin:$kind,$resourceId]" + } + + override fun contentToString(): String = "" public companion object Key : AbstractMessageKey({ it.safeCast() }) +} + +/** + * 消息来源 + * + * @suppress 随着更新, 元素数量会增加. 类名不稳定. + * + * @since 2.3 + */ +@MiraiExperimentalApi("RichMessageKind 类名不稳定") +public enum class RichMessageKind { + /** + * 长消息 + */ + LONG, + + /** + * 合并转发 + */ + FORWARD, + + // TODO: 2021/2/3 MusicShare RichMessageKind } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 11217fb02..59c1b75ca 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -24,7 +24,7 @@ internal data class LongMessageInternal internal constructor(override val conten val bot = contact.bot.asQQAndroidBot() val long = Mirai.downloadLongMessage(bot, resId) - return RichMessageOrigin(resId) + long + return RichMessageOrigin(SimpleServiceMessage(serviceId, content), resId, RichMessageKind.LONG) + long } companion object Key : @@ -59,7 +59,7 @@ internal data class ForwardMessageInternal(override val content: String, val res val preview = titles val source = xmlFoot.findField("name") - return RichMessageOrigin(resId) + ForwardMessage( + return RichMessageOrigin(SimpleServiceMessage(serviceId, content), resId, RichMessageKind.FORWARD) + ForwardMessage( preview = preview, title = title, brief = brief,