Pārlūkot izejas kodu

Merge pull request #169 from mamoe/long-message

Support long message
Him188 6 gadi atpakaļ
vecāks
revīzija
6b332a7328
41 mainītis faili ar 775 papildinājumiem un 187 dzēšanām
  1. 1 1
      mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt
  2. 100 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
  3. 10 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
  4. 1 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/JceOld.kt
  5. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt
  6. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt
  7. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt
  8. 106 34
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
  9. 43 50
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
  10. 6 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
  11. 5 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt
  12. 9 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt
  13. 5 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt
  14. 72 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/LongMsg.kt
  15. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt
  16. 25 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgTransmit.kt
  17. 80 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt
  18. 4 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt
  19. 3 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt
  20. 10 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
  21. 1 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt
  22. 97 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
  23. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
  24. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt
  25. 2 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
  26. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt
  27. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt
  28. 4 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/input.kt
  29. 5 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt
  30. 9 9
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/numbers.kt
  31. 5 3
      mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt
  32. 17 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt
  33. 9 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt
  34. 20 5
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
  35. 2 16
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt
  36. 57 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
  37. 31 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt
  38. 1 13
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt
  39. 5 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt
  40. 6 1
      mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt
  41. 16 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt

+ 1 - 1
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt → mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.utils.cryptor
+package net.mamoe.mirai.qqandroid.utils.cryptor
 
 import android.annotation.SuppressLint
 import net.mamoe.mirai.utils.MiraiInternalAPI

+ 100 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt

@@ -33,18 +33,24 @@ import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl
 import net.mamoe.mirai.qqandroid.contact.QQImpl
 import net.mamoe.mirai.qqandroid.contact.checkIsGroupImpl
+import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
+import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
 import net.mamoe.mirai.qqandroid.message.OnlineFriendImageImpl
 import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl
 import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
+import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
+import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.encodeToString
+import net.mamoe.mirai.utils.io.toReadPacket
 import kotlin.collections.asSequence
 import kotlin.coroutines.CoroutineContext
+import kotlin.math.absoluteValue
+import kotlin.random.Random
 
 @OptIn(MiraiInternalAPI::class)
 internal expect class QQAndroidBot constructor(
@@ -360,6 +366,97 @@ internal abstract class QQAndroidBotBase constructor(
         return json.parse(GroupActiveData.serializer(), rep)
     }
 
+    @LowLevelAPI
+    @MiraiExperimentalAPI
+    override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) {
+        val chain = message.asMessageChain()
+        check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "message is too large" }
+        val group = getGroup(groupCode)
+
+        val source = MessageSourceFromSendFriend(
+            messageRandom = Random.nextInt().absoluteValue,
+            senderId = client.uin,
+            toUin = Group.calculateGroupUinByGroupCode(groupCode),
+            time = currentTimeSeconds,
+            groupId = groupCode,
+            originalMessage = chain,
+            sequenceId = client.atomicNextMessageSequenceId()
+            //   sourceMessage = message
+        )
+
+        // TODO: 2020/3/26 util 方法来添加单例元素
+        val toSend = buildMessageChain(chain.size) {
+            source.originalMessage.forEach {
+                if (it !is MessageSource) {
+                    add(it)
+                }
+            }
+            add(source)
+        }
+
+        network.run {
+            val data = toSend.calculateValidationDataForGroup(group)
+
+            val response =
+                MultiMsg.ApplyUp.createForGroupLongMessage(
+                    client = [email protected],
+                    messageData = data,
+                    dstUin = Group.calculateGroupUinByGroupCode(groupCode)
+                ).sendAndExpect<MultiMsg.ApplyUp.Response>()
+
+            val resId: String
+            when (response) {
+                is MultiMsg.ApplyUp.Response.MessageTooLarge ->
+                    error("message is too large")
+                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 = Group.calculateGroupUinByGroupCode(groupCode),
+                                msgId = 0,
+                                msgUkey = response.proto.msgUkey,
+                                needCache = 0,
+                                storeType = 2,
+                                msgContent = data.data
+                            )
+                        )
+                    ).toByteArray(LongMsg.ReqBody.serializer())
+
+                    HighwayHelper.uploadImage(
+                        client,
+                        serverIp = response.proto.uint32UpIp!!.first().toIpV4AddressString(),
+                        serverPort = response.proto.uint32UpPort!!.first(),
+                        ticket = response.proto.msgSig, // 104
+                        imageInput = body.toReadPacket(),
+                        inputSize = body.size,
+                        fileMd5 = MiraiPlatformUtils.md5(body),
+                        commandId = 27 // long msg
+                    )
+                }
+            }
+
+            group.sendMessage(
+                RichMessage.longMessage(
+                    brief = toSend.joinToString(limit = 30) {
+                        when (it) {
+                            is PlainText -> it.stringValue
+                            is At -> it.toString()
+                            else -> ""
+                        }
+                    },
+                    resId = resId,
+                    timeSeconds = source.time
+                )
+            )
+        }
+    }
+
     override suspend fun queryImageUrl(image: Image): String = when (image) {
         is OnlineFriendImageImpl -> image.originUrl
         is OnlineGroupImageImpl -> image.originUrl

+ 10 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt

@@ -65,6 +65,8 @@ internal class GroupImpl(
     companion object;
 
     override val bot: QQAndroidBot by bot.unsafeWeakRef()
+
+    @OptIn(LowLevelAPI::class)
     val uin: Long = groupInfo.uin
 
     override lateinit var owner: Member
@@ -288,9 +290,13 @@ internal class GroupImpl(
                 source = it
                 source.startWaitingSequenceId(this)
             }.sendAndExpect()
-            check(
-                response is MessageSvc.PbSendMsg.Response.SUCCESS
-            ) { "send message failed: $response" }
+            if (response is MessageSvc.PbSendMsg.Response.Failed) {
+                when (response.resultType) {
+                    120 -> error("bot is being muted.")
+                    34 -> error("internal error: send message failed, illegal arguments: $response")
+                    else -> error("send message failed: $response")
+                }
+            }
         }
 
         return MessageReceipt(source, this, botAsMember)
@@ -349,7 +355,7 @@ internal class GroupImpl(
                             imageInput = image.input,
                             inputSize = image.inputSize.toInt(),
                             fileMd5 = image.md5,
-                            uKey = response.uKey,
+                            ticket = response.uKey,
                             commandId = 2
                         )
                     } ?: error("timeout uploading image: ${image.filename}")

+ 1 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/JceOld.kt

@@ -18,10 +18,8 @@ import kotlinx.serialization.builtins.SetSerializer
 import kotlinx.serialization.internal.*
 import kotlinx.serialization.modules.EmptyModule
 import kotlinx.serialization.modules.SerialModule
-import kotlinx.serialization.protobuf.ProtoId
 import net.mamoe.mirai.qqandroid.io.JceStruct
 import net.mamoe.mirai.qqandroid.io.ProtoBuf
-import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce
 import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.BYTE
 import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.DOUBLE
 import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.FLOAT
@@ -39,7 +37,7 @@ import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.STRUCT_END
 import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.ZERO_TYPE
 import net.mamoe.mirai.qqandroid.io.serialization.jce.JceHead
 import net.mamoe.mirai.qqandroid.io.serialization.jce.JceId
-import net.mamoe.mirai.utils.io.readString
+import net.mamoe.mirai.qqandroid.utils.io.readString
 import net.mamoe.mirai.utils.io.toReadPacket
 
 @PublishedApi

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt

@@ -11,7 +11,7 @@ package net.mamoe.mirai.qqandroid.io.serialization.jce
 
 import kotlinx.io.core.*
 import net.mamoe.mirai.qqandroid.io.serialization.JceCharset
-import net.mamoe.mirai.utils.io.readString
+import net.mamoe.mirai.qqandroid.utils.io.readString
 
 
 /**

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt

@@ -25,7 +25,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.firstValue
 import net.mamoe.mirai.utils.io.read
-import net.mamoe.mirai.utils.io.readPacketExact
+import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
 import net.mamoe.mirai.utils.io.toReadPacket
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt

@@ -254,7 +254,7 @@ internal class MessageSourceFromSendGroup(
     override val groupId: Long,
     override val originalMessage: MessageChain
 ) : MessageSourceFromSend() {
-    private lateinit var sequenceIdDeferred: Deferred<Int>
+    internal lateinit var sequenceIdDeferred: Deferred<Int>
 
     @OptIn(ExperimentalCoroutinesApi::class)
     override val id: Long

+ 106 - 34
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt

@@ -6,6 +6,7 @@
  *
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
+@file: OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalUnsignedTypes::class)
 
 package net.mamoe.mirai.qqandroid.message
 
@@ -218,6 +219,8 @@ private val atAllData = ImMsgBody.Elem(
     )
 )
 
+private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
+
 @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
 internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> {
     val elements = mutableListOf<ImMsgBody.Elem>()
@@ -231,33 +234,54 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
         }
     }
 
+    var longTextResId: String? = null
 
     fun transformOneMessage(it: Message) {
+        if (it is RichMessage) {
+            val content = MiraiPlatformUtils.zip(it.content.toByteArray())
+            when (it) {
+                is LongMessage -> {
+                    check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
+                    elements.add(
+                        ImMsgBody.Elem(
+                            richMsg = ImMsgBody.RichMsg(
+                                serviceId = 35, // ok
+                                template1 = byteArrayOf(1) + content
+                            )
+                        )
+                    )
+                    transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
+                    longTextResId = it.resId
+                }
+                is LightApp -> elements.add(
+                    ImMsgBody.Elem(
+                        lightApp = ImMsgBody.LightAppElem(
+                            data = byteArrayOf(1) + content
+                        )
+                    )
+                )
+                else -> elements.add(
+                    ImMsgBody.Elem(
+                        richMsg = ImMsgBody.RichMsg(
+                            serviceId = when (it) {
+                                is XmlMessage -> 60
+                                is JsonMessage -> 1
+                                //   is MergedForwardedMessage -> 35
+                                else -> error("unsupported RichMessage: ${it::class.simpleName}")
+                            },
+                            template1 = byteArrayOf(1) + content
+                        )
+                    )
+                )
+            }
+        }
+
         when (it) {
             is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
             is At -> {
                 elements.add(ImMsgBody.Elem(text = it.toJceData()))
                 elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
             }
-            is LightApp -> elements.add(
-                ImMsgBody.Elem(
-                    lightApp = ImMsgBody.LightAppElem(
-                        data = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray())
-                    )
-                )
-            )
-            is RichMessage -> elements.add(
-                ImMsgBody.Elem(
-                    richMsg = ImMsgBody.RichMsg(
-                        serviceId = when (it) {
-                            is XmlMessage -> 60
-                            is JsonMessage -> 1
-                            else -> error("unsupported RichMessage")
-                        },
-                        template1 = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray())
-                    )
-                )
-            )
             is OfflineGroupImage -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
             is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
             is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
@@ -267,16 +291,18 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
             is QuoteReplyToSend -> {
                 if (forGroup) {
                     check(it is QuoteReplyToSend.ToGroup) {
-                        "sending a quote to group using QuoteReplyToSend.ToFriend"
+                        "sending a quote to group using QuoteReplyToSend.ToFriend is prohibited"
                     }
                     if (it.sender is Member) {
                         transformOneMessage(it.createAt())
                     }
-                    transformOneMessage(" ".toMessage())
+                    transformOneMessage(PlainText(" "))
                 }
             }
-            is QuoteReply,
-            is MessageSource -> {
+            is QuoteReply, // already transformed above
+            is MessageSource, // mirai only
+            is RichMessage, // already transformed above
+            -> {
 
             }
             else -> error("unsupported message type: ${it::class.simpleName}")
@@ -284,10 +310,24 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
     }
     this.forEach(::transformOneMessage)
 
-    if (this.any<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 = "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())))
-    } else elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
+    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.any<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 = "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())))
+        }
+        else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
+    }
 
     return elements
 }
@@ -358,7 +398,7 @@ internal fun MsgComm.Msg.toMessageChain(): MessageChain {
     return buildMessageChain(elements.size + 1) {
         +MessageSourceFromMsg(delegate = this@toMessageChain)
         elements.joinToMessageChain(this)
-    }.removeAtIfHasQuoteReply()
+    }.cleanupRubbishMessageElements()
 }
 
 // These two functions are not identical, dont combine.
@@ -369,11 +409,40 @@ internal fun ImMsgBody.SourceMsg.toMessageChain(): MessageChain {
     return buildMessageChain(elements.size + 1) {
         +MessageSourceFromServer(delegate = this@toMessageChain)
         elements.joinToMessageChain(this)
-    }.removeAtIfHasQuoteReply()
+    }.cleanupRubbishMessageElements()
+}
+
+private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
+    var last: SingleMessage? = null
+    return buildMessageChain(initialSize = this.count()) {
+        [email protected] { element ->
+            if (last == null) {
+                last = element
+                return@forEach
+            } else {
+                if (last is LongMessage && element is PlainText) {
+                    if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
+                        last = element
+                        return@forEach
+                    }
+                }
+            }
+
+            add(element)
+            last = element
+        }
+    }
+}
+
+internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
+    this.forEach {
+        if (it is R) {
+            return it
+        }
+    }
+    throw NoSuchElementException("Collection contains no element matching the predicate.")
 }
 
-private fun MessageChain.removeAtIfHasQuoteReply(): MessageChain =
-    this
 /*
     if (this.any<QuoteReply>()) {
         var removed = false
@@ -387,9 +456,6 @@ private fun MessageChain.removeAtIfHasQuoteReply(): MessageChain =
         }.asMessageChain()
     } else this*/
 
-@OptIn(
-    MiraiInternalAPI::class, ExperimentalUnsignedTypes::class, MiraiDebugAPI::class, LowLevelAPI::class
-)
 internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
     this.forEach {
         when {
@@ -425,6 +491,12 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
                 when (it.richMsg.serviceId) {
                     1 -> message.add(JsonMessage(content))
                     60 -> message.add(XmlMessage(content))
+                    35 -> message.add(
+                        LongMessage(
+                            content,
+                            this.firstIsInstance<ImMsgBody.GeneralFlags>().longTextResid
+                        )
+                    )
                     else -> {
                         @Suppress("DEPRECATION")
                         MiraiLogger.debug {

+ 43 - 50
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt

@@ -39,8 +39,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.ByteArrayPool
 import net.mamoe.mirai.utils.io.PlatformSocket
-import net.mamoe.mirai.utils.io.readPacketExact
-import net.mamoe.mirai.utils.io.useBytes
+import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
+import net.mamoe.mirai.qqandroid.utils.io.useBytes
 import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.Volatile
 import kotlin.time.ExperimentalTime
@@ -262,32 +262,32 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                                 bot.groups.delegate.addLast(
                                     @Suppress("DuplicatedCode")
                                     (GroupImpl(
-        bot = bot,
-        coroutineContext = bot.coroutineContext,
-        id = troopNum.groupCode,
-        groupInfo = bot._lowLevelQueryGroupInfo(troopNum.groupCode).apply {
-            this as GroupInfoImpl
-
-            if (this.delegate.groupName == null) {
-                this.delegate.groupName = troopNum.groupName
-            }
-
-            if (this.delegate.groupMemo == null) {
-                this.delegate.groupMemo = troopNum.groupMemo
-            }
-
-            if (this.delegate.groupUin == null) {
-                this.delegate.groupUin = troopNum.groupUin
-            }
-
-            this.delegate.groupCode = troopNum.groupCode
-        },
-        members = bot._lowLevelQueryGroupMemberList(
-            troopNum.groupUin,
-            troopNum.groupCode,
-            troopNum.dwGroupOwnerUin
-        )
-    ))
+                                        bot = bot,
+                                        coroutineContext = bot.coroutineContext,
+                                        id = troopNum.groupCode,
+                                        groupInfo = bot._lowLevelQueryGroupInfo(troopNum.groupCode).apply {
+                                            this as GroupInfoImpl
+
+                                            if (this.delegate.groupName == null) {
+                                                this.delegate.groupName = troopNum.groupName
+                                            }
+
+                                            if (this.delegate.groupMemo == null) {
+                                                this.delegate.groupMemo = troopNum.groupMemo
+                                            }
+
+                                            if (this.delegate.groupUin == null) {
+                                                this.delegate.groupUin = troopNum.groupUin
+                                            }
+
+                                            this.delegate.groupCode = troopNum.groupCode
+                                        },
+                                        members = bot._lowLevelQueryGroupMemberList(
+                                            troopNum.groupUin,
+                                            troopNum.groupCode,
+                                            troopNum.dwGroupOwnerUin
+                                        )
+                                    ))
                                 )
                             }?.let {
                                 logger.error { "群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" }
@@ -581,32 +581,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
         check([email protected]) { "network is dead therefore can't send any packet" }
 
         suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E {
-            val result = async {
-                withTimeoutOrNull(3000) {
-                    withContext([email protected] + CoroutineName("Packet sender")) {
-                        PacketLogger.debug { "Channel sending: $commandName" }
-                        when (data) {
-                            is ByteArray -> channel.send(data, 0, length)
-                            is ByteReadPacket -> channel.send(data)
-                            else -> error("Internal error: unexpected data type: ${data::class.simpleName}")
-                        }
-                        PacketLogger.debug { "Channel send done: $commandName" }
+            withTimeoutOrNull(3000) {
+                withContext([email protected] + CoroutineName("Packet sender")) {
+                    PacketLogger.debug { "Channel sending: $commandName" }
+                    when (data) {
+                        is ByteArray -> channel.send(data, 0, length)
+                        is ByteReadPacket -> channel.send(data)
+                        else -> error("Internal error: unexpected data type: ${data::class.simpleName}")
                     }
-                } ?: return@async "timeout sending packet $commandName"
+                    PacketLogger.debug { "Channel send done: $commandName" }
+                }
+            } ?: throw TimeoutException("timeout sending packet $commandName")
 
-                logger.verbose("Send done: $commandName")
-
-                withTimeoutOrNull(timeoutMillis) {
-                    handler.await()
-                    // 不要 `withTimeout`. timeout 的报错会不正常.
-                } ?: return@async "timeout receiving response of $commandName"
-            }
+            logger.verbose("Send done: $commandName")
 
             @Suppress("UNCHECKED_CAST")
-            when (val value = result.await()) {
-                is String -> throw TimeoutException(value)
-                else -> return value as E
-            }
+            return withTimeoutOrNull(timeoutMillis) {
+                handler.await()
+                // 不要 `withTimeout`. timeout 的报错会不正常.
+            } as E? ?:  throw TimeoutException("timeout receiving response of $commandName")
         }
 
         if (retry == 0) {

+ 6 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt

@@ -22,9 +22,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
 import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv
 import net.mamoe.mirai.qqandroid.utils.NetworkType
+import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.cryptor.ECDH
-import net.mamoe.mirai.utils.cryptor.TEA
+import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
 import net.mamoe.mirai.utils.io.*
 
 /*
@@ -101,7 +101,7 @@ internal open class QQAndroidClient(
     var openAppId: Long = 715019303L
 
     val apkVersionName: ByteArray get() = "8.2.7".toByteArray()
-    val buildVer: String get() = "8.2.7.4410"
+    val buildVer: String get() = "8.2.7.4410" // 8.2.0.1296
 
     private val messageSequenceId: AtomicInt = atomic(22911)
     internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2)
@@ -115,6 +115,9 @@ internal open class QQAndroidClient(
     private val highwayDataTransSequenceIdForFriend: AtomicInt = atomic(43973)
     internal fun nextHighwayDataTransSequenceIdForFriend(): Int = highwayDataTransSequenceIdForFriend.getAndAdd(2)
 
+    private val highwayDataTransSequenceIdForApplyUp: AtomicInt = atomic(77918)
+    internal fun nextHighwayDataTransSequenceIdForApplyUp(): Int = highwayDataTransSequenceIdForApplyUp.getAndAdd(2)
+
     val appClientVersion: Int = 0
 
     var networkType: NetworkType = NetworkType.WIFI

+ 5 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt

@@ -33,7 +33,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.copyAndClose
 import net.mamoe.mirai.utils.io.ByteArrayPool
 import net.mamoe.mirai.utils.io.PlatformSocket
-import net.mamoe.mirai.utils.io.withUse
+import net.mamoe.mirai.qqandroid.utils.io.withUse
 import kotlinx.serialization.InternalSerializationApi
 
 @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
@@ -101,7 +101,7 @@ internal object HighwayHelper {
         client: QQAndroidClient,
         serverIp: String,
         serverPort: Int,
-        uKey: ByteArray,
+        ticket: ByteArray,
         imageInput: Any,
         inputSize: Int,
         fileMd5: ByteArray,
@@ -109,8 +109,8 @@ internal object HighwayHelper {
     ) {
         require(imageInput is Input || imageInput is InputStream || imageInput is ByteReadChannel) { "unsupported imageInput: ${imageInput::class.simpleName}" }
         require(fileMd5.size == 16) { "bad md5. Required size=16, got ${fileMd5.size}" }
-        require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" }
-        require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" }
+      //  require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
+       // require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" }
 
         val socket = PlatformSocket()
         socket.connect(serverIp, serverPort)
@@ -119,7 +119,7 @@ internal object HighwayHelper {
                 client = client,
                 command = "PicUp.DataUp",
                 commandId = commandId,
-                uKey = uKey,
+                ticket = ticket,
                 data = imageInput,
                 dataSize = inputSize,
                 fileMd5 = fileMd5

+ 9 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt

@@ -36,7 +36,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
     dataFlag: Int = 4096,
     commandId: Int,
     localId: Int = 2052,
-    uKey: ByteArray,
+    ticket: ByteArray,
 
     data: Any,
     dataSize: Int,
@@ -45,7 +45,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
 ): Flow<ByteReadPacket> {
     ByteArrayPool.checkBufferSize(sizePerPacket)
     require(data is Input || data is InputStream || data is ByteReadChannel) { "unsupported data: ${data::class.simpleName}" }
-    require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" }
+ //   require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
     require(data !is ByteReadPacket || data.remaining.toInt() == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as ByteReadPacket).remaining}" }
 
     val flow = when (data) {
@@ -64,8 +64,12 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
                     version = 1,
                     uin = client.uin.toString(),
                     command = command,
-                    seq = if (commandId == 2) client.nextHighwayDataTransSequenceIdForGroup()
-                    else client.nextHighwayDataTransSequenceIdForFriend(),
+                    seq = when (commandId) {
+                        2 -> client.nextHighwayDataTransSequenceIdForGroup()
+                        1 -> client.nextHighwayDataTransSequenceIdForFriend()
+                        27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
+                        else -> error("illegal commandId: $commandId")
+                    },
                     retryTimes = 0,
                     appid = appId,
                     dataflag = dataFlag,
@@ -77,7 +81,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans
                     datalength = chunkedInput.bufferSize,
                     dataoffset = offset,
                     filesize = dataSize.toLong(),
-                    serviceticket = uKey,
+                    serviceticket = ticket,
                     md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize),
                     fileMd5 = fileMd5,
                     flag = 0,

+ 5 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt

@@ -17,7 +17,7 @@ import net.mamoe.mirai.qqandroid.io.ProtoBuf
 import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 
 @Serializable
-class BdhExtinfo : ProtoBuf {
+internal class BdhExtinfo : ProtoBuf {
     @Serializable
     class CommFileExtReq(
         @ProtoId(1) val actionType: Int = 0,
@@ -140,7 +140,7 @@ class BdhExtinfo : ProtoBuf {
 }
 
 @Serializable
-class CSDataHighwayHead : ProtoBuf {
+internal class CSDataHighwayHead : ProtoBuf {
     @Serializable
     class C2CCommonExtendinfo(
         @ProtoId(1) val infoId: Int = 0,
@@ -283,7 +283,7 @@ class CSDataHighwayHead : ProtoBuf {
 }
 
 @Serializable
-class HwConfigPersistentPB : ProtoBuf {
+internal class HwConfigPersistentPB : ProtoBuf {
     @Serializable
     class HwConfigItemPB(
         @ProtoId(1) val ingKey: String = "",
@@ -315,7 +315,7 @@ class HwConfigPersistentPB : ProtoBuf {
 }
 
 @Serializable
-class HwSessionInfoPersistentPB : ProtoBuf {
+internal class HwSessionInfoPersistentPB : ProtoBuf {
     @Serializable
     class HwSessionInfoPB(
         @ProtoId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
@@ -324,7 +324,7 @@ class HwSessionInfoPersistentPB : ProtoBuf {
 }
 
 @Serializable
-class Subcmd0x501 : ProtoBuf {
+internal class Subcmd0x501 : ProtoBuf {
     @Serializable
     class ReqBody(
         @ProtoId(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null

+ 72 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/LongMsg.kt

@@ -0,0 +1,72 @@
+package net.mamoe.mirai.qqandroid.network.protocol.data.proto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoId
+import net.mamoe.mirai.qqandroid.io.ProtoBuf
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+
+internal class LongMsg : ProtoBuf {
+    @Serializable
+    class MsgDeleteReq(
+        @ProtoId(1) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(2) val msgType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class MsgDeleteRsp(
+        @ProtoId(1) val result: Int = 0,
+        @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class MsgDownReq(
+        @ProtoId(1) val srcUin: Int = 0,
+        @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(3) val msgType: Int = 0,
+        @ProtoId(4) val needCache: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class MsgDownRsp(
+        @ProtoId(1) val result: Int = 0,
+        @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(3) val msgContent: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class MsgUpReq(
+        @ProtoId(1) val msgType: Int = 0,
+        @ProtoId(2) val dstUin: Long = 0L,
+        @ProtoId(3) val msgId: Int = 0,
+        @ProtoId(4) val msgContent: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(5) val storeType: Int = 0,
+        @ProtoId(6) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(7) val needCache: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class MsgUpRsp(
+        @ProtoId(1) val result: Int = 0,
+        @ProtoId(2) val msgId: Int = 0,
+        @ProtoId(3) val msgResid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ReqBody(
+        @ProtoId(1) val subcmd: Int = 0,
+        @ProtoId(2) val termType: Int = 0,
+        @ProtoId(3) val platformType: Int = 0,
+        @ProtoId(4) val msgUpReq: List<LongMsg.MsgUpReq>? = null,
+        @ProtoId(5) val msgDownReq: List<LongMsg.MsgDownReq>? = null,
+        @ProtoId(6) val msgDelReq: List<LongMsg.MsgDeleteReq>? = null,
+        @ProtoId(10) val agentType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class RspBody(
+        @ProtoId(1) val subcmd: Int = 0,
+        @ProtoId(2) val msgUpRsp: List<LongMsg.MsgUpRsp>? = null,
+        @ProtoId(3) val msgDownRsp: List<LongMsg.MsgDownRsp>? = null,
+        @ProtoId(4) val msgDelRsp: List<LongMsg.MsgDeleteRsp>? = null
+    ) : ProtoBuf
+}

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt

@@ -467,7 +467,7 @@ internal class ImMsgBody : ProtoBuf {
         @ProtoId(4) val rpId: ByteArray = EMPTY_BYTE_ARRAY,
         @ProtoId(5) val prpFold: Int = 0,
         @ProtoId(6) val longTextFlag: Int = 0,
-        @ProtoId(7) val longTextResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(7) val longTextResid: String = "",
         @ProtoId(8) val groupType: Int = 0,
         @ProtoId(9) val toUinFlag: Int = 0,
         @ProtoId(10) val glamourLevel: Int = 0,

+ 25 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgTransmit.kt

@@ -0,0 +1,25 @@
+package net.mamoe.mirai.qqandroid.network.protocol.data.proto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoId
+import net.mamoe.mirai.qqandroid.io.ProtoBuf
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+
+internal class MsgTransmit : ProtoBuf {
+    @Serializable
+    class PbMultiMsgItem(
+        @ProtoId(1) val fileName: String = "",
+        @ProtoId(2) val buffer: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class PbMultiMsgNew(
+        @ProtoId(1) val msg: List<MsgComm.Msg>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class PbMultiMsgTransmit(
+        @ProtoId(1) val msg: List<MsgComm.Msg>? = null,
+        @ProtoId(2) val pbItemList: List<MsgTransmit.PbMultiMsgItem>? = null
+    ) : ProtoBuf
+}

+ 80 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt

@@ -0,0 +1,80 @@
+package net.mamoe.mirai.qqandroid.network.protocol.data.proto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoId
+import net.mamoe.mirai.qqandroid.io.ProtoBuf
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+
+@Serializable
+internal class MultiMsg : ProtoBuf {
+    @Serializable
+    class ExternMsg(
+        @ProtoId(1) val channelType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class MultiMsgApplyDownReq(
+        @ProtoId(1) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(2) val msgType: Int = 0,
+        @ProtoId(3) val srcUin: Long = 0L
+    ) : ProtoBuf
+
+    @Serializable
+    class MultiMsgApplyDownRsp(
+        @ProtoId(1) val result: Int = 0,
+        @ProtoId(2) val thumbDownPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(3) val msgKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(4) val uint32DownIp: List<Int>? = null,
+        @ProtoId(5) val uint32DownPort: List<Int>? = null,
+        @ProtoId(6) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(7) val msgExternInfo: MultiMsg.ExternMsg? = null,
+        @ProtoId(8) val bytesDownIpV6: List<ByteArray>? = null,
+        @ProtoId(9) val uint32DownV6Port: List<Int>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class MultiMsgApplyUpReq(
+        @ProtoId(1) val dstUin: Long = 0L,
+        @ProtoId(2) val msgSize: Long = 0L,
+        @ProtoId(3) val msgMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(4) val msgType: Int = 0,
+        @ProtoId(5) val applyId: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class MultiMsgApplyUpRsp(
+        @ProtoId(1) val result: Int = 0,
+        @ProtoId(2) val msgResid: String = "",
+        @ProtoId(3) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(4) val uint32UpIp: List<Int>? = null,
+        @ProtoId(5) val uint32UpPort: List<Int>? = null,
+        @ProtoId(6) val blockSize: Long = 0L,
+        @ProtoId(7) val upOffset: Long = 0L,
+        @ProtoId(8) val applyId: Int = 0,
+        @ProtoId(9) val msgKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(10) val msgSig: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(11) val msgExternInfo: MultiMsg.ExternMsg? = null,
+        @ProtoId(12) val bytesUpIpV6: List<ByteArray>? = null,
+        @ProtoId(13) val uint32UpV6Port: List<Int>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class ReqBody(
+        @ProtoId(1) val subcmd: Int = 0,
+        @ProtoId(2) val termType: Int = 0,
+        @ProtoId(3) val platformType: Int = 0,
+        @ProtoId(4) val netType: Int = 0,
+        @ProtoId(5) val buildVer: String = "",
+        @ProtoId(6) val multimsgApplyupReq: List<MultiMsg.MultiMsgApplyUpReq>? = null,
+        @ProtoId(7) val multimsgApplydownReq: List<MultiMsg.MultiMsgApplyDownReq>? = null,
+        @ProtoId(8) val buType: Int = 0,
+        @ProtoId(9) val reqChannelType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class RspBody(
+        @ProtoId(1) val subcmd: Int = 0,
+        @ProtoId(2) val multimsgApplyupRsp: List<MultiMsg.MultiMsgApplyUpRsp>? = null,
+        @ProtoId(3) val multimsgApplydownRsp: List<MultiMsg.MultiMsgApplyDownRsp>? = null
+    ) : ProtoBuf
+}

+ 4 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt

@@ -14,10 +14,10 @@ import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.buildPacket
 import kotlinx.io.core.writeFully
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
-import net.mamoe.mirai.utils.cryptor.ECDH
-import net.mamoe.mirai.utils.cryptor.ECDHKeyPair
-import net.mamoe.mirai.utils.io.encryptAndWrite
-import net.mamoe.mirai.utils.io.writeShortLVByteArray
+import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH
+import net.mamoe.mirai.qqandroid.utils.cryptor.ECDHKeyPair
+import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite
+import net.mamoe.mirai.qqandroid.utils.io.writeShortLVByteArray
 
 @OptIn(ExperimentalUnsignedTypes::class)
 internal interface EncryptMethod {

+ 3 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt

@@ -16,9 +16,9 @@ import kotlinx.io.core.buildPacket
 import kotlinx.io.core.writeFully
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
 import net.mamoe.mirai.utils.MiraiInternalAPI
-import net.mamoe.mirai.utils.io.encryptAndWrite
-import net.mamoe.mirai.utils.io.writeHex
-import net.mamoe.mirai.utils.io.writeIntLVPacket
+import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite
+import net.mamoe.mirai.qqandroid.utils.io.writeHex
+import net.mamoe.mirai.qqandroid.utils.io.writeIntLVPacket
 
 internal class OutgoingPacket constructor(
     name: String?,

+ 10 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt

@@ -11,9 +11,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet
 
 import kotlinx.io.core.*
 import kotlinx.io.pool.useInstance
-import net.mamoe.mirai.qqandroid.network.Packet
 import net.mamoe.mirai.event.Event
 import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.network.Packet
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.MultiMsg
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
@@ -26,9 +27,13 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.qqandroid.network.readUShortLVByteArray
+import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
+import net.mamoe.mirai.qqandroid.utils.io.readString
+import net.mamoe.mirai.qqandroid.utils.io.useBytes
+import net.mamoe.mirai.qqandroid.utils.io.withUse
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.cryptor.TEA
-import net.mamoe.mirai.utils.cryptor.adjustToPublicKey
+import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
+import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
 import net.mamoe.mirai.utils.io.*
 import kotlin.jvm.JvmName
 
@@ -144,7 +149,8 @@ internal object KnownPacketFactories {
         TroopManagement.EditGroupNametag,
         TroopManagement.Kick,
         Heartbeat.Alive,
-        PbMessageSvc.PbMsgWithDraw
+        PbMessageSvc.PbMsgWithDraw,
+        MultiMsg.ApplyUp
     )
 
     object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

+ 1 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt

@@ -17,6 +17,7 @@ import kotlinx.io.core.toByteArray
 import kotlinx.io.core.writeFully
 import net.mamoe.mirai.qqandroid.network.protocol.LoginType
 import net.mamoe.mirai.qqandroid.utils.NetworkType
+import net.mamoe.mirai.qqandroid.utils.io.*
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.MiraiPlatformUtils
 import net.mamoe.mirai.utils.currentTimeMillis

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 97 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt


+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt

@@ -126,7 +126,7 @@ internal class MessageSvc {
 
         object EmptyResponse : GetMsgSuccess(emptyList())
 
-        @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class, FlowPreview::class)
+        @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class, FlowPreview::class, LowLevelAPI::class)
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
             // 00 00 01 0F 08 00 12 00 1A 34 08 FF C1 C4 F1 05 10 FF C1 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 8A CA 91 D1 0C 48 9B A5 BD 9B 0A 58 DE 9D 99 F8 08 60 1D 68 FF C1 C4 F1 05 70 00 20 02 2A 9D 01 08 F3 C1 C4 F1 05 10 A2 FF 8C F0 03 18 01 22 8A 01 0A 2A 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 A6 01 20 0B 28 AE F9 01 30 F4 C1 C4 F1 05 38 A7 E3 D8 D4 84 80 80 80 01 B8 01 CD B5 01 12 08 08 01 10 00 18 00 20 00 1A 52 0A 50 0A 27 08 00 10 F4 C1 C4 F1 05 18 A7 E3 D8 D4 04 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 08 0A 06 0A 04 4E 4D 53 4C 12 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 12 04 4A 02 08 00 30 01 2A 15 08 97 A2 C1 F1 05 10 95 A6 F5 E5 0C 18 01 30 01 40 01 48 81 01 2A 10 08 D3 F7 B5 F1 05 10 DD F1 92 B7 07 18 01 30 01 38 00 42 00 48 00
             val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer())

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt

@@ -38,7 +38,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.buildResponseUniPacket
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.debug
 import net.mamoe.mirai.utils.io.read
-import net.mamoe.mirai.utils.io.readString
+import net.mamoe.mirai.qqandroid.utils.io.readString
 import net.mamoe.mirai.utils.io.toUHexString
 
 internal class OnlinePush {

+ 2 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt

@@ -20,8 +20,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.*
 import net.mamoe.mirai.qqandroid.utils.GuidSource
 import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag
 import net.mamoe.mirai.qqandroid.utils.guidFlag
+import net.mamoe.mirai.qqandroid.utils.io.*
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.cryptor.TEA
+import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
 import net.mamoe.mirai.utils.io.*
 
 internal class WtLogin {

+ 1 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.utils.cryptor
+package net.mamoe.mirai.qqandroid.utils.cryptor
 
 import net.mamoe.mirai.utils.io.chunkedHexToBytes
 

+ 1 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/TEA.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.utils.cryptor
+package net.mamoe.mirai.qqandroid.utils.cryptor
 
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.pool.useInstance

+ 4 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/input.kt

@@ -11,7 +11,7 @@
 @file:JvmMultifileClass
 @file:JvmName("Utils")
 
-package net.mamoe.mirai.utils.io
+package net.mamoe.mirai.qqandroid.utils.io
 
 import kotlinx.io.OutputStream
 import kotlinx.io.charsets.Charset
@@ -27,6 +27,9 @@ import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.jvm.JvmSynthetic
 import kotlinx.serialization.InternalSerializationApi
+import net.mamoe.mirai.utils.io.ByteArrayPool
+import net.mamoe.mirai.utils.io.toReadPacket
+import net.mamoe.mirai.utils.io.toUHexString
 
 @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class)
 fun ByteReadPacket.copyTo(outputStream: OutputStream) {

+ 5 - 5
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt

@@ -11,12 +11,12 @@
 @file:JvmMultifileClass
 @file:JvmName("Utils")
 
-package net.mamoe.mirai.utils.io
+package net.mamoe.mirai.qqandroid.utils.io
 
 import kotlinx.io.core.*
+import net.mamoe.mirai.qqandroid.utils.coerceAtMostOrFail
+import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
 import net.mamoe.mirai.utils.MiraiInternalAPI
-import net.mamoe.mirai.utils.coerceAtMostOrFail
-import net.mamoe.mirai.utils.cryptor.TEA
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 
@@ -41,7 +41,7 @@ inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int {
 inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
     BytePacketBuilder().apply(builder).build().use {
         if (tag != null) writeUByte(tag)
-        val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL)
+        val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
         writeInt(length.toInt())
         writePacket(it)
         return length.toInt()
@@ -50,7 +50,7 @@ inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset:
 inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
     BytePacketBuilder().apply(builder).build().use {
         if (tag != null) writeUByte(tag)
-        val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL)
+        val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
         writeUShort(length.toUShort())
         writePacket(it)
         return length.toInt()

+ 9 - 9
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt → mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/numbers.kt

@@ -10,7 +10,7 @@
 @file:JvmMultifileClass
 @file:JvmName("Utils")
 
-package net.mamoe.mirai.utils
+package net.mamoe.mirai.qqandroid.utils
 
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
@@ -18,8 +18,8 @@ import kotlin.jvm.JvmName
 /**
  * 要求 [this] 最小为 [min].
  */
-@Suppress("NOTHING_TO_INLINE")
-inline fun Int.coerceAtLeastOrFail(min: Int): Int {
+@PublishedApi
+internal fun Int.coerceAtLeastOrFail(min: Int): Int {
     require(this >= min)
     return this
 }
@@ -27,8 +27,8 @@ inline fun Int.coerceAtLeastOrFail(min: Int): Int {
 /**
  * 要求 [this] 最小为 [min].
  */
-@Suppress("NOTHING_TO_INLINE")
-inline fun Long.coerceAtLeastOrFail(min: Long): Long {
+@PublishedApi
+internal fun Long.coerceAtLeastOrFail(min: Long): Long {
     require(this >= min)
     return this
 }
@@ -36,12 +36,12 @@ inline fun Long.coerceAtLeastOrFail(min: Long): Long {
 /**
  * 要求 [this] 最大为 [max].
  */
-@Suppress("NOTHING_TO_INLINE")
-inline fun Int.coerceAtMostOrFail(max: Int): Int =
+@PublishedApi
+internal fun Int.coerceAtMostOrFail(max: Int): Int =
     if (this >= max) error("value is greater than its expected maximum value $max")
     else this
 
-@Suppress("NOTHING_TO_INLINE")
-inline fun Long.coerceAtMostOrFail(max: Long): Long =
+@PublishedApi
+internal fun Long.coerceAtMostOrFail(max: Long): Long =
     if (this >= max) error("value is greater than its expected maximum value $max")
     else this

+ 5 - 3
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt → mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.utils.cryptor
+package net.mamoe.mirai.qqandroid.utils.cryptor
 
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.MiraiPlatformUtils
@@ -27,11 +27,13 @@ internal actual class ECDHKeyPairImpl(
     override val privateKey: ECDHPrivateKey get() = delegate.private
     override val publicKey: ECDHPublicKey get() = delegate.public
 
-    override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
+    override val initialShareKey: ByteArray =
+        ECDH.calculateShareKey(privateKey, initialPublicKey)
 }
 
 @Suppress("FunctionName")
-actual fun ECDH() = ECDH(ECDH.generateKeyPair())
+actual fun ECDH() =
+    ECDH(ECDH.generateKeyPair())
 
 actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
     actual companion object {

+ 17 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt

@@ -21,6 +21,8 @@ import java.io.InputStream
 import java.net.Inet4Address
 import java.security.MessageDigest
 import java.util.zip.Deflater
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
 import java.util.zip.Inflater
 
 
@@ -63,6 +65,7 @@ actual object MiraiPlatformUtils {
         }
     }
 
+
     actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
         data.checkOffsetAndLength(offset, length)
         return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()
@@ -99,4 +102,18 @@ actual object MiraiPlatformUtils {
             block(read)
         }
     }
+
+    actual fun gzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        ByteArrayOutputStream().use { buf ->
+            GZIPOutputStream(buf).use { gzip ->
+                data.inputStream(offset, length).use { t -> t.copyTo(gzip) }
+            }
+            buf.flush()
+            return buf.toByteArray()
+        }
+    }
+
+    actual fun ungzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() }
+    }
 }

+ 9 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt

@@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.QQ
 import net.mamoe.mirai.data.*
+import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.MessageSource
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
 import net.mamoe.mirai.utils.MiraiInternalAPI
@@ -139,6 +140,14 @@ interface LowLevelBotAPIAccessor {
     @LowLevelAPI
     @MiraiExperimentalAPI
     suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData
+
+    /**
+     * 发送长消息
+     */
+    @SinceMirai("0.31.0")
+    @LowLevelAPI
+    @MiraiExperimentalAPI
+    suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message)
 }
 
 /**

+ 20 - 5
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt

@@ -16,6 +16,7 @@ package net.mamoe.mirai.message.data
 import net.mamoe.mirai.message.data.NullMessageChain.equals
 import net.mamoe.mirai.message.data.NullMessageChain.toString
 import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.SinceMirai
 import kotlin.js.JsName
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
@@ -45,6 +46,12 @@ interface MessageChain : Message, Iterable<SingleMessage> {
     override operator fun contains(sub: String): Boolean
     override fun toString(): String
 
+    /**
+     * 元素数量
+     */
+    @SinceMirai("0.31.1")
+    val size: Int
+
     /**
      * 获取第一个类型为 [key] 的 [Message] 实例
      *
@@ -374,7 +381,6 @@ inline fun MessageChain.flatten(): Sequence<SingleMessage> = this.asSequence() /
 
 // endregion converters
 
-// region implementations
 
 /**
  * 不含任何元素的 [MessageChain]
@@ -389,19 +395,25 @@ object EmptyMessageChain : MessageChain by MessageChainImplByIterable(emptyList(
  */
 object NullMessageChain : MessageChain {
     override fun toString(): String = "NullMessageChain"
+    override val size: Int get() = 0
     override fun equals(other: Any?): Boolean = other === this
     override fun contains(sub: String): Boolean = error("accessing NullMessageChain")
     override fun followedBy(tail: Message): CombinedMessage = CombinedMessage(left = EmptyMessageChain, tail = tail)
     override fun iterator(): MutableIterator<SingleMessage> = error("accessing NullMessageChain")
 }
 
+
+// region implementations
+
+
 /**
  * 使用 [Iterable] 作为委托的 [MessageChain]
  */
 @PublishedApi
-internal inline class MessageChainImplByIterable constructor(
+internal class MessageChainImplByIterable constructor(
     private val delegate: Iterable<SingleMessage>
 ) : Message, Iterable<SingleMessage>, MessageChain {
+    override val size: Int by lazy { delegate.count() }
     override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
     override fun toString(): String = this.delegate.joinToString("") { it.toString() }
     override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
@@ -411,9 +423,10 @@ internal inline class MessageChainImplByIterable constructor(
  * 使用 [Collection] 作为委托的 [MessageChain]
  */
 @PublishedApi
-internal inline class MessageChainImplByCollection constructor(
+internal class MessageChainImplByCollection constructor(
     private val delegate: Collection<SingleMessage>
 ) : Message, Iterable<SingleMessage>, MessageChain {
+    override val size: Int get() = delegate.size
     override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
     override fun toString(): String = this.delegate.joinToString("") { it.toString() }
     override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
@@ -426,11 +439,12 @@ internal inline class MessageChainImplByCollection constructor(
 internal class MessageChainImplBySequence constructor(
     delegate: Sequence<SingleMessage>
 ) : Message, Iterable<SingleMessage>, MessageChain {
+    override val size: Int by lazy { collected.size }
+
     /**
      * [Sequence] 可能只能消耗一遍, 因此需要先转为 [List]
      */
     private val collected: List<SingleMessage> by lazy { delegate.toList() }
-
     override fun iterator(): Iterator<SingleMessage> = collected.iterator()
     override fun toString(): String = this.collected.joinToString("") { it.toString() }
     override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) }
@@ -440,9 +454,10 @@ internal class MessageChainImplBySequence constructor(
  * 单个 [SingleMessage] 作为 [MessageChain]
  */
 @PublishedApi
-internal inline class SingleMessageChainImpl constructor(
+internal class SingleMessageChainImpl constructor(
     private val delegate: SingleMessage
 ) : Message, Iterable<SingleMessage>, MessageChain {
+    override val size: Int get() = 1
     override fun toString(): String = this.delegate.toString()
     override fun iterator(): Iterator<SingleMessage> = iterator { yield(delegate) }
     override operator fun contains(sub: String): Boolean = sub in delegate

+ 2 - 16
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt

@@ -15,7 +15,6 @@ package net.mamoe.mirai.message.data
 
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
-import kotlin.jvm.JvmStatic
 import kotlin.jvm.JvmSynthetic
 
 /**
@@ -28,26 +27,13 @@ class PlainText(val stringValue: String) :
     Comparable<String> by stringValue,
     CharSequence by stringValue {
 
+    @Suppress("unused")
     constructor(charSequence: CharSequence) : this(charSequence.toString())
 
     override operator fun contains(sub: String): Boolean = sub in stringValue
     override fun toString(): String = stringValue
 
-    companion object Key : Message.Key<PlainText> {
-        @JvmStatic
-        val Empty = PlainText("")
-
-        @JvmStatic
-        val Null = PlainText("null")
-
-        inline fun of(value: String): PlainText {
-            return PlainText(value)
-        }
-
-        inline fun of(value: CharSequence): PlainText {
-            return PlainText(value)
-        }
-    }
+    companion object Key : Message.Key<PlainText>
 }
 
 /**

+ 57 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt

@@ -13,6 +13,7 @@
 package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.SinceMirai
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
@@ -33,6 +34,48 @@ interface RichMessage : MessageContent {
     @SinceMirai("0.30.0")
     companion object Templates : Message.Key<RichMessage> {
 
+        /**
+         * 合并转发.
+         */
+        @MiraiExperimentalAPI
+        fun mergedForward(): Nothing {
+            TODO()
+        }
+
+        /**
+         * 长消息.
+         *
+         * @param brief 消息内容纯文本, 显示在图片的前面
+         */
+        @SinceMirai("0.31.0")
+        @OptIn(MiraiInternalAPI::class)
+        @MiraiExperimentalAPI
+        fun longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage {
+            val limited: String = if (brief.length > 30) {
+                brief.take(30) + "…"
+            } else {
+                brief
+            }
+
+            val template = """
+                <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+                <msg serviceID="35" templateID="1" action="viewMultiMsg"
+                     brief="$limited"
+                     m_resid="$resId"
+                     m_fileName="$timeSeconds" sourceMsgId="0" url=""
+                     flag="3" adverSign="0" multiMsgFlag="1">
+                    <item layout="1">
+                        <title>$limited</title>
+                        <hr hidden="false" style="0"/>
+                        <summary>点击查看完整消息</summary>
+                    </item>
+                    <source name="聊天记录" icon="" action="" appid="-1"/>
+                </msg>
+            """.trimIndent()
+
+            return LongMessage(template, resId)
+        }
+
         @MiraiExperimentalAPI
         @SinceMirai("0.30.0")
         fun share(url: String, title: String? = null, content: String? = null, coverUrl: String? = null): XmlMessage =
@@ -107,6 +150,20 @@ class XmlMessage constructor(override val content: String) : RichMessage {
     override fun toString(): String = content
 }
 
+/**
+ * 长消息
+ */
+@SinceMirai("0.31.0")
+@MiraiExperimentalAPI
+@MiraiInternalAPI
+class LongMessage(override val content: String, val resId: String) : RichMessage {
+    companion object Key : Message.Key<XmlMessage>
+
+    // serviceId = 35
+    override fun toString(): String = content
+}
+
+
 /**
  * 构造一条 XML 消息
  */

+ 31 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt

@@ -11,6 +11,7 @@ package net.mamoe.mirai.utils
 
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.network.LoginFailedException
 import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmStatic
 
@@ -18,10 +19,33 @@ import kotlin.jvm.JvmStatic
  * 验证码, 设备锁解决器
  */
 expect abstract class LoginSolver {
+    /**
+     * 处理图片验证码.
+     * 返回 null 以表示无法处理验证码, 将会刷新验证码或重试登录.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
+     *
+     * @throws LoginFailedException
+     */
     abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
 
+    /**
+     * 处理滑动验证码.
+     * 返回 null 以表示无法处理验证码, 将会刷新验证码或重试登录.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
+     *
+     * @throws LoginFailedException
+     * @return 验证码解决成功后获得的 ticket.
+     */
     abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
 
+    /**
+     * 处理不安全设备验证.
+     * 在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
+     *
+     * @return 任意内容. 返回值保留以供未来更新.
+     * @throws LoginFailedException
+     */
     abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
 
     companion object {
@@ -38,10 +62,12 @@ expect open class BotConfiguration() {
      * 日志记录器
      */
     var botLoggerSupplier: ((Bot) -> MiraiLogger)
+
     /**
      * 网络层日志构造器
      */
     var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger)
+
     /**
      * 设备信息覆盖. 默认使用随机的设备信息.
      */
@@ -56,23 +82,28 @@ expect open class BotConfiguration() {
      * 心跳周期. 过长会导致被服务器断开连接.
      */
     var heartbeatPeriodMillis: Long
+
     /**
      * 每次心跳时等待结果的时间.
      * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
      */
     var heartbeatTimeoutMillis: Long
+
     /**
      * 心跳失败后的第一次重连前的等待时间.
      */
     var firstReconnectDelayMillis: Long
+
     /**
      * 重连失败后, 继续尝试的每次等待时间
      */
     var reconnectPeriodMillis: Long
+
     /**
      * 最多尝试多少次重连
      */
     var reconnectionRetryTimes: Int
+
     /**
      * 验证码处理器
      */

+ 1 - 13
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt

@@ -11,8 +11,6 @@
 
 package net.mamoe.mirai.utils.io
 
-import kotlinx.io.core.IoBuffer
-import kotlinx.io.pool.ObjectPool
 import kotlin.random.Random
 import kotlin.random.nextInt
 
@@ -201,14 +199,4 @@ fun ByteArray.toUShort(): UShort =
 fun ByteArray.toInt(): Int =
     (this[0].toInt().and(255) shl 24) + (this[1].toInt().and(255) shl 16) + (this[2].toInt().and(255) shl 8) + (this[3].toInt().and(
         255
-    ) shl 0)
-
-/**
- * 从 [IoBuffer.Pool] [borrow][ObjectPool.borrow] 一个 [IoBuffer] 然后将 [this] 写入.
- * 注意回收 ([ObjectPool.recycle])
- */
-fun ByteArray.toIoBuffer(
-    offset: Int = 0,
-    length: Int = this.size - offset,
-    pool: ObjectPool<IoBuffer> = IoBuffer.Pool
-): IoBuffer = pool.borrow().let { it.writeFully(this, offset, length); it }
+    ) shl 0)

+ 5 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt

@@ -14,7 +14,7 @@ package net.mamoe.mirai.utils
 import io.ktor.client.HttpClient
 
 /**
- * 时间戳
+ * 时间戳.
  */
 expect val currentTimeMillis: Long
 
@@ -30,6 +30,10 @@ expect object MiraiPlatformUtils {
 
     fun zip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
 
+    fun gzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
+
+    fun ungzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
+
 
     fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray
 

+ 6 - 1
mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt

@@ -14,11 +14,16 @@ import net.mamoe.mirai.utils.io.encodeToString
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
+@OptIn(MiraiInternalAPI::class)
 internal class PlatformUtilsTest {
 
-    @OptIn(MiraiInternalAPI::class)
     @Test
     fun testZip() {
         assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString())
     }
+
+    @Test
+    fun testGZip() {
+        assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString())
+    }
 }

+ 16 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt

@@ -22,6 +22,8 @@ import java.io.OutputStream
 import java.net.Inet4Address
 import java.security.MessageDigest
 import java.util.zip.Deflater
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
 import java.util.zip.Inflater
 
 /**
@@ -64,6 +66,20 @@ actual object MiraiPlatformUtils {
         }
     }
 
+    actual fun gzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        ByteArrayOutputStream().use { buf ->
+            GZIPOutputStream(buf).use { gzip ->
+                data.inputStream(offset, length).use { t -> t.copyTo(gzip) }
+            }
+            buf.flush()
+            return buf.toByteArray()
+        }
+    }
+
+    actual fun ungzip(data: ByteArray, offset: Int, length: Int): ByteArray {
+        return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() }
+    }
+
     actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray {
         data.checkOffsetAndLength(offset, length)
         return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels