Browse Source

Rewrite message to struct conversions, add ReceiveMessageTransformer, for parsing long and forward messages in the future

Him188 5 năm trước cách đây
mục cha
commit
1768872bab

+ 1 - 1
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()
 
         /**
          * 在一段时间后撤回这条消息.

+ 7 - 0
mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt

@@ -20,6 +20,13 @@ public inline fun <reified T> Any?.safeCast(): T? = this as? T
 
 public inline fun <reified T> Any?.castOrNull(): T? = this as? T
 
+public inline fun <reified R> 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

+ 2 - 1
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")

+ 48 - 0
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()
+}

+ 413 - 0
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<MsgComm.Msg>.toMessageChainOnline(
+    bot: Bot,
+    groupIdOrZero: Long,
+    messageSourceKind: MessageSourceKind
+): MessageChain {
+    return toMessageChain(bot, bot.id, groupIdOrZero, true, messageSourceKind)
+}
+
+internal fun List<MsgComm.Msg>.toMessageChainOffline(
+    bot: Bot,
+    groupIdOrZero: Long,
+    messageSourceKind: MessageSourceKind
+): MessageChain {
+    return toMessageChain(bot, bot.id, groupIdOrZero, false, messageSourceKind)
+}
+
+internal fun List<MsgComm.Msg>.toMessageChainNoSource(
+    botId: Long,
+    groupIdOrZero: Long,
+    messageSourceKind: MessageSourceKind
+): MessageChain {
+    return toMessageChain(null, botId, groupIdOrZero, null, messageSourceKind)
+}
+
+private fun List<MsgComm.Msg>.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<MsgComm.Msg>,
+    ): 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<ImMsgBody.Elem>,
+        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()) {
+            [email protected] { 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群
+            /*
+                        <?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
+    阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
+    30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
+    浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
+    ="" a_actionData="" url=""/></msg>
+                         */
+            /**
+             * 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)
+    )
+}

+ 41 - 0
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")
[email protected]
+internal inline fun <R> 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)
+    }
+}

+ 0 - 674
mirai-core/src/commonMain/kotlin/message/conversions.kt

@@ -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<ImMsgBody.Elem> {
-    val forGroup = messageTarget is Group
-    val elements = ArrayList<ImMsgBody.Elem>(this.size)
-
-    if (this.anyIsInstance<QuoteReply>()) {
-        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<CustomMessage>,
-                                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<MarketFaceImpl>() -> {
-                elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE)))
-            }
-            this.anyIsInstance<RichMessage>() -> {
-                // 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<FlashImage>() -> {
-                elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU)))
-            }
-            this.anyIsInstance<PttMessage>() -> {
-                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<MsgOnlinePush.PbPushMsg>.toMessageChain(
-    bot: Bot,
-    groupIdOrZero: Long,
-    onlineSource: Boolean,
-    messageSourceKind: MessageSourceKind
-): MessageChain = map { it.msg }.toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, messageSourceKind)
-
-internal fun List<MsgComm.Msg>.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<Message> {
-        [email protected] { 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()) {
-        [email protected] { 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 <reified R> 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<ImMsgBody.Elem>.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群
-                    /*
-                    <?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
-阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
-30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
-浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
-="" a_actionData="" url=""/></msg>
-                     */
-                    /**
-                     * json?
-                     */
-                    1 -> @Suppress("DEPRECATION_ERROR")
-                    list.add(SimpleServiceMessage(1, content))
-                    /**
-                     * [LongMessageInternal], [ForwardMessage]
-                     */
-                    35 -> {
-                        val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.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")
[email protected]
-internal inline fun <R> 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)
-    }
-}

+ 87 - 123
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<MsgComm.Msg>
+    msg: List<MsgComm.Msg>
 ) : 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<MsgComm.Msg>
+    msg: List<MsgComm.Msg>
 ) : 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<MsgComm.Msg>.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<MsgComm.Msg>
+    msg: List<MsgComm.Msg>
 ) : 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<MsgComm.Msg>
+    msg: List<MsgComm.Msg>
 ) : 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
+    }
+}

+ 289 - 0
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<ImMsgBody.Elem> {
+    val forGroup = messageTarget is Group
+    val elements = ArrayList<ImMsgBody.Elem>(this.size)
+
+    if (this.anyIsInstance<QuoteReply>()) {
+        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<CustomMessage>,
+                                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<MarketFaceImpl>() -> {
+                elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE)))
+            }
+            this.anyIsInstance<RichMessage>() -> {
+                // 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<FlashImage>() -> {
+                elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU)))
+            }
+            this.anyIsInstance<PttMessage>() -> {
+                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()

+ 6 - 22
mirai-core/src/commonMain/kotlin/message/offlineSourceImpl.kt

@@ -106,40 +106,24 @@ internal class OfflineMessageSourceImplData(
 }
 
 internal fun OfflineMessageSourceImplData(
-    bot: Bot?,
+    bot: Bot,
     delegate: List<MsgComm.Msg>,
-    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

+ 14 - 24
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
                             )
                         }

+ 3 - 13
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<Packet?>("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<Packet?>("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
             )