Jelajahi Sumber

Rewrite Group sendMessageImpl, improve current Highway impls, for music share #889 #682 and further friend message highway #194, #577

Him188 5 tahun lalu
induk
melakukan
d60006376c

+ 0 - 13
mirai-core-api/src/commonMain/kotlin/message/data/RichMessage.kt

@@ -265,17 +265,4 @@ public class XmlMessageBuilder(
             this.builder.append("<picture cover='$coverUrl'/>")
         }
     }
-}
-
-// internal runtime value, not serializable
-internal data class LongMessage internal constructor(override val content: String, val resId: String) :
-    AbstractServiceMessage() {
-    override val serviceId: Int get() = 35
-
-    companion object Key : AbstractPolymorphicMessageKey<ServiceMessage, LongMessage>(ServiceMessage, { it.safeCast() })
-}
-
-// internal runtime value, not serializable
-internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() {
-    override val serviceId: Int get() = 35
 }

+ 52 - 79
mirai-core/src/commonMain/kotlin/MiraiImpl.kt

@@ -24,7 +24,7 @@ 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.HighwayHelper
+import net.mamoe.mirai.internal.network.highway.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.packet.chat.*
@@ -32,7 +32,6 @@ 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.utils.io.serialization.toByteArray
-import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.action.Nudge
 import net.mamoe.mirai.message.data.*
@@ -690,102 +689,76 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
         return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
     }
 
-    @JvmSynthetic
-    @LowLevelApi
-    @MiraiExperimentalApi
-    @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-    internal suspend fun lowLevelSendGroupLongOrForwardMessage(
+    internal suspend fun uploadGroupMessageHighway(
         bot: Bot,
         groupCode: Long,
         message: Collection<ForwardMessage.INode>,
         isLong: Boolean,
-        forwardMessage: ForwardMessage?
-    ): MessageReceipt<Group> = with(bot.asQQAndroidBot()) {
+    ): String = with(bot.asQQAndroidBot()) {
         message.forEach {
             it.messageChain.ensureSequenceIdAvailable()
         }
 
         val group = getGroupOrFail(groupCode)
 
-        val time = currentTimeSeconds()
         val sequenceId = client.atomicNextMessageSequenceId()
 
-        network.run {
-            val data = message.calculateValidationDataForGroup(
-                sequenceId = sequenceId,
-                random = Random.nextInt().absoluteValue,
-                group
-            )
+        val data = message.calculateValidationDataForGroup(
+            sequenceId = sequenceId,
+            random = Random.nextInt().absoluteValue,
+            group
+        )
 
-            val response =
-                MultiMsg.ApplyUp.createForGroupLongMessage(
-                    buType = if (isLong) 1 else 2,
-                    client = bot.client,
-                    messageData = data,
-                    dstUin = Mirai.calculateGroupUinByGroupCode(groupCode)
-                ).sendAndExpect<MultiMsg.ApplyUp.Response>()
-
-            val resId: String
-            when (response) {
-                is MultiMsg.ApplyUp.Response.MessageTooLarge ->
-                    error(
-                        "Internal error: message is too large, but this should be handled before sending. "
-                    )
-                is MultiMsg.ApplyUp.Response.RequireUpload -> {
-                    resId = response.proto.msgResid
-
-                    val body = LongMsg.ReqBody(
-                        subcmd = 1,
-                        platformType = 9,
-                        termType = 5,
-                        msgUpReq = listOf(
-                            LongMsg.MsgUpReq(
-                                msgType = 3, // group
-                                dstUin = Mirai.calculateGroupUinByGroupCode(groupCode),
-                                msgId = 0,
-                                msgUkey = response.proto.msgUkey,
-                                needCache = 0,
-                                storeType = 2,
-                                msgContent = data.data
-                            )
-                        )
-                    ).toByteArray(LongMsg.ReqBody.serializer())
-
-                    HighwayHelper.uploadImageToServers(
-                        bot,
-                        response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
-                        response.proto.msgSig,
-                        body.toExternalResource(null),
-                        "group long message",
-                        27
-                    )
-                }
-            }
+        val response = network.run {
+            MultiMsg.ApplyUp.createForGroup(
+                buType = if (isLong) 1 else 2,
+                client = bot.client,
+                messageData = data,
+                dstUin = Mirai.calculateGroupUinByGroupCode(groupCode)
+            ).sendAndExpect<MultiMsg.ApplyUp.Response>()
+        }
 
-            if (isLong) {
-                group.sendMessage(
-                    RichMessage.longMessage(
-                        brief = message.joinToString(limit = 27) { it.messageChain.contentToString() },
-                        resId = resId,
-                        timeSeconds = time
-                    )
+        val resId: String
+        when (response) {
+            is MultiMsg.ApplyUp.Response.MessageTooLarge ->
+                error(
+                    "Internal error: message is too large, but this should be handled before sending. "
                 )
-            } else {
-                checkNotNull(forwardMessage) { "Internal error: forwardMessage is null when sending forward" }
-                group.sendMessage(
-                    RichMessage.forwardMessage(
-                        resId = resId,
-                        timeSeconds = time,
-                        //  preview = message.take(5).joinToString {
-                        //      """
-                        //          <title size="26" color="#777777" maxLines="2" lineSpace="12">${it.message.asMessageChain().joinToString(limit = 10)}</title>
-                        //      """.trimIndent()
-                        //  },
-                        forwardMessage = forwardMessage,
+            is MultiMsg.ApplyUp.Response.RequireUpload -> {
+                resId = response.proto.msgResid
+
+                val body = LongMsg.ReqBody(
+                    subcmd = 1,
+                    platformType = 9,
+                    termType = 5,
+                    msgUpReq = listOf(
+                        LongMsg.MsgUpReq(
+                            msgType = 3, // group
+                            dstUin = Mirai.calculateGroupUinByGroupCode(groupCode),
+                            msgId = 0,
+                            msgUkey = response.proto.msgUkey,
+                            needCache = 0,
+                            storeType = 2,
+                            msgContent = data.data
+                        )
                     )
+                ).toByteArray(LongMsg.ReqBody.serializer())
+
+                Highway.uploadResource(
+                    bot,
+                    response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
+                    response.proto.msgSig,
+                    body.toExternalResource(null),
+                    when (isLong) {
+                        true -> "group long message"
+                        false -> "group forward message"
+                    },
+                    27
                 )
             }
         }
+
+        return resId
     }
 
 

+ 2 - 2
mirai-core/src/commonMain/kotlin/QQAndroidBot.kt

@@ -131,7 +131,7 @@ internal class QQAndroidBot constructor(
 
 internal val EMPTY_BYTE_ARRAY = ByteArray(0)
 
-internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage {
+internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSeconds: Long): LongMessageInternal {
     val limited: String = if (brief.length > 30) {
         brief.take(30) + "…"
     } else {
@@ -154,7 +154,7 @@ internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSecon
                 </msg>
             """.trimIndent()
 
-    return LongMessage(template, resId)
+    return LongMessageInternal(template, resId)
 }
 
 

+ 6 - 139
mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt

@@ -18,15 +18,11 @@ import net.mamoe.mirai.data.GroupInfo
 import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
-import net.mamoe.mirai.internal.MiraiImpl
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.message.*
-import net.mamoe.mirai.internal.message.firstIsInstanceOrNull
 import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
-import net.mamoe.mirai.internal.network.highway.HighwayHelper
+import net.mamoe.mirai.internal.network.highway.Highway
 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
-import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
-import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
 import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
@@ -118,140 +114,11 @@ internal class GroupImpl(
         require(!message.isContentEmpty()) { "message is empty" }
         check(!isBotMuted) { throw BotIsBeingMutedException(this) }
 
-        return sendMessageImpl(message, false).also {
-            logMessageSent(message)
-        }
-    }
-
-    private suspend fun sendMessageImpl(message: Message, isForward: Boolean): MessageReceipt<Group> {
-        if (message is MessageChain) {
-            if (message.anyIsInstance<ForwardMessage>()) {
-                return sendMessageImpl(message.singleOrNull() ?: error("ForwardMessage must be standalone"), true)
-            }
-        }
-
-        val forward = message as? ForwardMessage ?: message.castOrNull<MessageChain>()?.findIsInstance<ForwardMessage>()
-        if (forward != null) {
-            check(forward.nodeList.size < 200) {
-                throw MessageTooLargeException(
-                    this, forward, forward,
-                    "ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
-                )
-            }
-
-            return MiraiImpl.lowLevelSendGroupLongOrForwardMessage(bot, this.id, forward.nodeList, false, forward)
-        }
-
-        val isLongOrForward = message is LongMessage || message is ForwardMessageInternal
-        val msg: MessageChain = if (!isLongOrForward) {
-            val chain = kotlin.runCatching {
-                GroupMessagePreSendEvent(this, message).broadcast()
-            }.onSuccess {
-                check(!it.isCancelled) {
-                    throw EventCancelledException("cancelled by GroupMessagePreSendEvent")
-                }
-            }.getOrElse {
-                throw EventCancelledException("exception thrown when broadcasting GroupMessagePreSendEvent", it)
-            }.message.toMessageChain()
-
-            @Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER")
-            var length = 0
-
-            @Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER") // stupid compiler
-            var imageCnt = 0
-            chain.verityLength(message, this, lengthCallback = {
-                length = it
-            }, imageCntCallback = {
-                imageCnt = it
-            })
-
-            if (length > 702 || imageCnt > 1) {  // 阈值为700左右,限制到3的倍数
-                return MiraiImpl.lowLevelSendGroupLongOrForwardMessage(
-                    bot,
-                    this.id,
-                    listOf(
-                        ForwardMessage.Node(
-                            senderId = bot.id,
-                            time = currentTimeSeconds().toInt(),
-                            messageChain = chain,
-                            senderName = bot.nick
-                        )
-                    ),
-                    true, null
-                )
-            }
-            chain
-        } else message.toMessageChain()
-
-        msg.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
+        val chain = broadcastGroupMessagePreSendEvent(message)
 
-        msg.filterIsInstance<FriendImage>().forEach { image ->
-            bot.network.run {
-                ImgStore.GroupPicUp(
-                    bot.client,
-                    uin = bot.id,
-                    groupCode = id,
-                    md5 = image.md5,
-                    size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0
-                ).sendAndExpect<ImgStore.GroupPicUp.Response>()
-            }
-        }
-
-        val result = bot.network.runCatching sendMsg@{
-            val source: OnlineMessageSourceToGroupImpl
-            MessageSvcPbSendMsg.createToGroup(
-                bot.client,
-                this@GroupImpl,
-                msg,
-                isForward
-            ) {
-                source = it
-            }.sendAndExpect<MessageSvcPbSendMsg.Response>().let {
-                if (!isLongOrForward && it is MessageSvcPbSendMsg.Response.MessageTooLarge) {
-                    return@sendMsg MiraiImpl.lowLevelSendGroupLongOrForwardMessage(
-                        bot,
-                        [email protected],
-                        listOf(
-                            ForwardMessage.Node(
-                                senderId = bot.id,
-                                time = currentTimeSeconds().toInt(),
-                                messageChain = msg,
-                                senderName = bot.nick
-                            )
-                        ),
-                        true, null
-                    )
-                }
-                check(it is MessageSvcPbSendMsg.Response.SUCCESS) {
-                    "Send group message failed: $it"
-                }
-            }
-
-            try {
-                source.ensureSequenceIdAvailable()
-            } catch (e: Exception) {
-                bot.network.logger.warning {
-                    "Timeout awaiting sequenceId for group message(${
-                        message.contentToString()
-                            .take(10)
-                    }). Some features may not work properly"
-                }
-                bot.network.logger.warning(e)
-            }
-
-            MessageReceipt(source, this@GroupImpl)
+        return sendMessageImpl(message, chain, false).also {
+            logMessageSent(chain)
         }
-
-        result.fold(
-            onSuccess = {
-                GroupMessagePostSendEvent(this, msg, null, it)
-            },
-            onFailure = {
-                GroupMessagePostSendEvent(this, msg, it, null)
-            }
-        ).broadcast()
-
-        return result.getOrThrow()
     }
 
     @OptIn(ExperimentalTime::class)
@@ -280,7 +147,7 @@ internal class GroupImpl(
                         .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
                 }
                 is ImgStore.GroupPicUp.Response.RequireUpload -> {
-                    HighwayHelper.uploadImageToServers(
+                    Highway.uploadResource(
                         bot,
                         response.uploadIpList.zip(response.uploadPortList),
                         response.uKey,
@@ -304,7 +171,7 @@ internal class GroupImpl(
             val response: PttStore.GroupPttUp.Response.RequireUpload =
                 PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()
 
-            HighwayHelper.uploadPttToServers(
+            Highway.uploadPttToServers(
                 bot,
                 response.uploadIpList.zip(response.uploadPortList),
                 resource,

+ 194 - 0
mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt

@@ -0,0 +1,194 @@
+/*
+ * 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.contact
+
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.MessageTooLargeException
+import net.mamoe.mirai.event.broadcast
+import net.mamoe.mirai.event.events.EventCancelledException
+import net.mamoe.mirai.event.events.GroupMessagePostSendEvent
+import net.mamoe.mirai.event.events.GroupMessagePreSendEvent
+import net.mamoe.mirai.internal.MiraiImpl
+import net.mamoe.mirai.internal.forwardMessage
+import net.mamoe.mirai.internal.longMessage
+import net.mamoe.mirai.internal.message.*
+import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.currentTimeSeconds
+
+/**
+ * Might be recalled with [transformedMessage] `is` [LongMessageInternal] if length estimation failed ([sendMessagePacket])
+ */
+internal suspend fun GroupImpl.sendMessageImpl(
+    originalMessage: Message,
+    transformedMessage: Message,
+    forceAsLongMessage: Boolean,
+): MessageReceipt<Group> {
+    val chain = transformedMessage
+        .transformSpecialMessages(this)
+        .convertToLongMessageIfNeeded(originalMessage, forceAsLongMessage, this)
+
+    chain.findIsInstance<QuoteReply>()?.source?.ensureSequenceIdAvailable()
+
+    chain.asSequence().filterIsInstance<FriendImage>().forEach { image ->
+        updateFriendImageForGroupMessage(image)
+    }
+
+    return sendMessagePacket(
+        originalMessage,
+        chain,
+        allowResendAsLongMessage = transformedMessage.takeSingleContent<LongMessageInternal>() == null
+    )
+}
+
+
+/**
+ * Called only in 'public' apis.
+ */
+internal suspend fun GroupImpl.broadcastGroupMessagePreSendEvent(message: Message): MessageChain {
+    return kotlin.runCatching {
+        GroupMessagePreSendEvent(this, message).broadcast()
+    }.onSuccess {
+        check(!it.isCancelled) {
+            throw EventCancelledException("cancelled by GroupMessagePreSendEvent")
+        }
+    }.getOrElse {
+        throw EventCancelledException("exception thrown when broadcasting GroupMessagePreSendEvent", it)
+    }.message.toMessageChain()
+}
+
+
+/**
+ * - [ForwardMessage] -> [ForwardMessageInternal] (by uploading through highway)
+ * - ... any others for future
+ */
+private suspend fun Message.transformSpecialMessages(contact: Contact): MessageChain {
+    return takeSingleContent<ForwardMessage>()?.let { forward ->
+        check(forward.nodeList.size <= 200) {
+            throw MessageTooLargeException(
+                contact, forward, forward,
+                "ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
+            )
+        }
+
+        val resId = MiraiImpl.uploadGroupMessageHighway(contact.bot, contact.id, forward.nodeList, false)
+        RichMessage.forwardMessage(
+            resId = resId,
+            timeSeconds = currentTimeSeconds(),
+            forwardMessage = forward,
+        )
+    }?.toMessageChain() ?: toMessageChain()
+}
+
+/**
+ * Final process
+ */
+private suspend fun GroupImpl.sendMessagePacket(
+    originalMessage: Message,
+    finalMessage: MessageChain,
+    allowResendAsLongMessage: Boolean,
+): MessageReceipt<Group> {
+    val group = this
+
+    val result = bot.network.runCatching sendMsg@{
+        val source: OnlineMessageSourceToGroupImpl
+        MessageSvcPbSendMsg.createToGroup(bot.client, group, finalMessage) {
+            source = it
+        }.sendAndExpect<MessageSvcPbSendMsg.Response>().let { resp ->
+            if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
+                if (allowResendAsLongMessage) {
+                    return@sendMsg sendMessageImpl(originalMessage, finalMessage, true)
+                } else throw MessageTooLargeException(
+                    group,
+                    originalMessage,
+                    finalMessage,
+                    "Message '${finalMessage.content.take(10)}' is too large."
+                )
+            }
+            check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
+                "Send group message failed: $resp"
+            }
+        }
+
+        try {
+            source.ensureSequenceIdAvailable()
+        } catch (e: Exception) {
+            bot.network.logger.warning(
+                "Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
+                e
+
+            )
+        }
+
+        MessageReceipt(source, group)
+    }
+
+    GroupMessagePostSendEvent(this, finalMessage, result.exceptionOrNull(), result.getOrNull()).broadcast()
+
+    return result.getOrThrow()
+}
+
+private suspend fun GroupImpl.uploadGroupLongMessageHighway(
+    chain: MessageChain
+) = MiraiImpl.uploadGroupMessageHighway(
+    bot, this.id,
+    listOf(
+        ForwardMessage.Node(
+            senderId = bot.id,
+            time = currentTimeSeconds().toInt(),
+            messageChain = chain,
+            senderName = bot.nick
+        )
+    ),
+    true
+)
+
+private suspend fun MessageChain.convertToLongMessageIfNeeded(
+    originalMessage: Message,
+    forceAsLongMessage: Boolean,
+    groupImpl: GroupImpl,
+): MessageChain {
+    if (forceAsLongMessage || this.shouldSendAsLongMessage(originalMessage, groupImpl)) {
+        val resId = groupImpl.uploadGroupLongMessageHighway(this)
+
+        return this + RichMessage.longMessage(
+            brief = takeContent(27),
+            resId = resId,
+            timeSeconds = currentTimeSeconds()
+        ) // LongMessageInternal replaces all contents and preserves metadata
+    }
+
+    return this
+}
+
+/**
+ * Ensures server holds the cache
+ */
+private suspend fun GroupImpl.updateFriendImageForGroupMessage(image: FriendImage) {
+    bot.network.run {
+        ImgStore.GroupPicUp(
+            bot.client,
+            uin = bot.id,
+            groupCode = id,
+            md5 = image.md5,
+            size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0
+        ).sendAndExpect<ImgStore.GroupPicUp.Response>()
+    }
+}
+
+private fun MessageChain.shouldSendAsLongMessage(originalMessage: Message, target: Contact): Boolean {
+    val length = verityLength(originalMessage, target)
+
+    return length > 700 || countImages() > 1
+}

+ 20 - 15
mirai-core/src/commonMain/kotlin/contact/util.kt

@@ -16,6 +16,7 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.internal.asQQAndroidBot
+import net.mamoe.mirai.internal.message.LongMessageInternal
 import net.mamoe.mirai.internal.message.OnlineMessageSourceToFriendImpl
 import net.mamoe.mirai.internal.message.OnlineMessageSourceToStrangerImpl
 import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
@@ -26,6 +27,7 @@ import net.mamoe.mirai.internal.utils.estimateLength
 import net.mamoe.mirai.message.*
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.cast
+import net.mamoe.mirai.utils.castOrNull
 import net.mamoe.mirai.utils.verbose
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
@@ -52,7 +54,7 @@ internal suspend fun <T : User> Friend.sendMessageImpl(
     }.getOrElse {
         throw EventCancelledException("exception thrown when broadcasting FriendMessagePreSendEvent", it)
     }.message.toMessageChain()
-    chain.verityLength(message, this, {}, {})
+    chain.verityLength(message, this)
 
     chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
 
@@ -104,7 +106,7 @@ internal suspend fun <T : User> Stranger.sendMessageImpl(
     }.getOrElse {
         throw EventCancelledException("exception thrown when broadcasting StrangerMessagePreSendEvent", it)
     }.message.toMessageChain()
-    chain.verityLength(message, this, {}, {})
+    chain.verityLength(message, this)
 
     chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
 
@@ -138,32 +140,27 @@ internal suspend fun <T : User> Stranger.sendMessageImpl(
 }
 
 internal fun Contact.logMessageSent(message: Message) {
-    if (message !is LongMessage) {
+    if (message !is LongMessageInternal) {
         bot.logger.verbose("$this <- $message".replaceMagicCodes())
     }
 }
 
-internal inline fun MessageChain.verityLength(
-    message: Message, target: Contact,
-    lengthCallback: (Int) -> Unit,
-    imageCntCallback: (Int) -> Unit
-) {
-    contract {
-        callsInPlace(lengthCallback, InvocationKind.EXACTLY_ONCE)
-        callsInPlace(imageCntCallback, InvocationKind.EXACTLY_ONCE)
-    }
+internal fun MessageChain.countImages(): Int = this.count { it is Image }
 
+internal fun MessageChain.verityLength(
+    originalMessage: Message, target: Contact,
+): Int {
     val chain = this
     val length = estimateLength(target, 15001)
-    lengthCallback(length)
-    if (length > 15000 || count { it is Image }.apply { imageCntCallback(this) } > 50) {
+    if (length > 15000 || countImages() > 50) {
         throw MessageTooLargeException(
-            target, message, this,
+            target, originalMessage, this,
             "message(${
                 chain.joinToString("", limit = 10)
             }) is too large. Allow up to 50 images or 5000 chars"
         )
     }
+    return length
 }
 
 @Suppress("RemoveRedundantQualifierName") // compiler bug
@@ -214,3 +211,11 @@ internal fun String.applyCharMapping() = buildString(capacity = this.length) {
 
 internal fun String.replaceMagicCodes(): String = this
     .applyCharMapping()
+
+
+internal fun Message.takeContent(length: Int): String =
+    this.toMessageChain().joinToString("", limit = length) { it.content }
+
+internal inline fun <reified T : MessageContent> Message.takeSingleContent(): T? {
+    return this as? T ?: this.castOrNull<MessageChain>()?.findIsInstance()
+}

+ 32 - 0
mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt

@@ -0,0 +1,32 @@
+/*
+ * 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
+ */
+
+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
+
+// internal runtime value, not serializable
+internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) :
+    AbstractServiceMessage() {
+    override val serviceId: Int get() = 35
+
+    companion object Key :
+        AbstractPolymorphicMessageKey<ServiceMessage, LongMessageInternal>(ServiceMessage, { it.safeCast() })
+}
+
+// internal runtime value, not serializable
+internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() {
+    override val serviceId: Int get() = 35
+
+    companion object Key :
+        AbstractPolymorphicMessageKey<ServiceMessage, ForwardMessageInternal>(ServiceMessage, { it.safeCast() })
+}

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

@@ -72,7 +72,7 @@ internal fun MessageChain.toRichTextElems(
                     )
                     transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
                 }
-                is LongMessage -> {
+                is LongMessageInternal -> {
                     check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
                     elements.add(
                         ImMsgBody.Elem(
@@ -375,7 +375,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
     return buildMessageChain(initialSize = this.count()) {
         [email protected] { element ->
             @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-            if (last is LongMessage && element is PlainText) {
+            if (last is LongMessageInternal && element is PlainText) {
                 if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
                     previousLast = last
                     last = element
@@ -535,7 +535,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(
                     1 -> @Suppress("DEPRECATION_ERROR")
                     list.add(SimpleServiceMessage(1, content))
                     /**
-                     * [LongMessage], [ForwardMessage]
+                     * [LongMessageInternal], [ForwardMessage]
                      */
                     35 -> {
                         val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.longTextResid

+ 39 - 0
mirai-core/src/commonMain/kotlin/network/highway/ChunkedFlowSession.kt

@@ -0,0 +1,39 @@
+/*
+ * 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
+ */
+
+package net.mamoe.mirai.internal.network.highway
+
+import kotlinx.io.core.Closeable
+import net.mamoe.mirai.utils.runBIO
+import net.mamoe.mirai.utils.withUse
+import java.io.InputStream
+
+internal class ChunkedFlowSession<T>(
+    private val input: InputStream,
+    private val buffer: ByteArray,
+    private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
+) : Closeable {
+    override fun close() {
+        input.close()
+    }
+
+    private var offset = 0L
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
+        runBIO {
+            while (true) {
+                val size = input.read(buffer)
+                if (size == -1) return@runBIO
+                block(mapper(buffer, size, offset))
+                offset += size
+            }
+        }
+    }
+}

+ 5 - 34
mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt → mirai-core/src/commonMain/kotlin/network/highway/Highway.kt

@@ -15,7 +15,6 @@ import io.ktor.http.*
 import io.ktor.http.content.*
 import io.ktor.utils.io.*
 import io.ktor.utils.io.jvm.javaio.*
-import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withTimeoutOrNull
@@ -34,7 +33,6 @@ import net.mamoe.mirai.internal.utils.toIpV4AddressString
 import net.mamoe.mirai.utils.*
 import java.io.InputStream
 import kotlin.math.roundToInt
-import kotlin.time.ExperimentalTime
 import kotlin.time.measureTime
 
 
@@ -83,10 +81,9 @@ internal suspend fun HttpClient.postImage(
 } == HttpStatusCode.OK
 
 
-internal object HighwayHelper {
-    @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-    @OptIn(ExperimentalTime::class)
-    suspend fun uploadImageToServers(
+internal object Highway {
+
+    suspend fun uploadResource(
         bot: QQAndroidBot,
         servers: List<Pair<Int, Int>>,
         uKey: ByteArray,
@@ -104,7 +101,7 @@ internal object HighwayHelper {
         }
 
         val time = measureTime {
-            uploadImage(
+            uploadResourceImpl(
                 client = bot.client,
                 serverIp = ip,
                 serverPort = port,
@@ -120,9 +117,7 @@ internal object HighwayHelper {
         }
     }
 
-    @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
-    @OptIn(InternalCoroutinesApi::class)
-    internal suspend fun uploadImage(
+    private suspend fun uploadResourceImpl(
         client: QQAndroidClient,
         serverIp: String,
         serverPort: Int,
@@ -213,30 +208,6 @@ internal object HighwayHelper {
     }
 }
 
-internal class ChunkedFlowSession<T>(
-    private val input: InputStream,
-    private val buffer: ByteArray,
-    private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
-) : Closeable {
-    override fun close() {
-        input.close()
-    }
-
-    private var offset = 0L
-
-    @Suppress("BlockingMethodInNonBlockingContext")
-    internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
-        runBIO {
-            while (true) {
-                val size = input.read(buffer)
-                if (size == -1) return@runBIO
-                block(mapper(buffer, size, offset))
-                offset += size
-            }
-        }
-    }
-}
-
 
 internal fun createImageDataPacketSequence(
     // RequestDataTrans

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt

@@ -102,7 +102,7 @@ internal class MultiMsg {
                     if (PacketLogger.isEnabled) {
                         return _miraiContentToString()
                     }
-                    return "MultiMsg.ApplyUp.Response.RequireUpload(proto=$proto)"
+                    return "MultiMsg.ApplyUp.Response.RequireUpload"
                 }
             }
 
@@ -110,7 +110,7 @@ internal class MultiMsg {
         }
 
         // captured from group
-        fun createForGroupLongMessage(
+        fun createForGroup(
             buType: Int,
             client: QQAndroidClient,
             messageData: MessageValidationData,

+ 4 - 6
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt

@@ -295,7 +295,6 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
         client: QQAndroidClient,
         targetGroup: Group,
         message: MessageChain,
-        isForward: Boolean,
         source: OnlineMessageSourceToGroupImpl
     ): OutgoingPacket = buildOutgoingUniPacket(client) {
         ///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
@@ -333,9 +332,10 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
                 msgRand = source.internalIds.single(),
                 syncCookie = EMPTY_BYTE_ARRAY,
                 msgVia = 1,
-                msgCtrl = if (isForward) MsgCtrl.MsgCtrl(
-                    msgFlag = 4
-                ) else null
+                msgCtrl =
+                if (message[ForwardMessageInternal] != null)
+                    MsgCtrl.MsgCtrl(msgFlag = 4)
+                else null
             )
         )
     }
@@ -429,7 +429,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup(
     client: QQAndroidClient,
     group: Group,
     message: MessageChain,
-    isForward: Boolean,
     crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit
 ): OutgoingPacket {
     contract {
@@ -457,7 +456,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup(
         client,
         group,
         message,
-        isForward,
         source
     )
 }