Forráskód Böngészése

Support long messages

Him188 6 éve
szülő
commit
a7e9b151e4
17 módosított fájl, 322 hozzáadás és 164 törlés
  1. 81 11
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
  2. 5 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
  3. 51 19
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
  4. 41 48
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
  5. 3 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
  6. 4 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt
  7. 9 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt
  8. 5 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt
  9. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt
  10. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt
  11. 54 34
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
  12. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
  13. 29 19
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
  14. 31 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt
  15. 1 13
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt
  16. 4 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt
  17. 1 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt

+ 81 - 11
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt

@@ -33,18 +33,20 @@ 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.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.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
@@ -367,27 +369,95 @@ internal abstract class QQAndroidBotBase constructor(
     @LowLevelAPI
     @MiraiExperimentalAPI
     override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) {
+        val chain = message.asMessageChain()
+        check(chain.toString().length <= 3000 && chain.count { it is Image } <= 10) { "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 = message.asMessageChain(),
-            sequenceId = 0
+            originalMessage = chain,
+            sequenceId = client.atomicNextMessageSequenceId()
             //   sourceMessage = message
         )
 
         // TODO: 2020/3/26 util 方法来添加单例元素
-        val toSend = buildMessageChain {
-            source.originalMessage.filter { it !is MessageSource }.forEach {
-                add(it)
+        val toSend = buildMessageChain(chain) {
+            source.originalMessage.forEach {
+                if (it !is MessageSource){
+                    add(it)
+                }
             }
             add(source)
         }
+
         network.run {
-            val response = MultiMsg.ApplyUp.createForLongMessage([email protected], toSend, groupCode)
-                .sendAndExpect<MultiMsg.ApplyUp.Response>()
+            val data = toSend.calculateValidationDataForGroup(group)
+
+            val response =
+                MultiMsg.ApplyUp.createForLongMessage(
+                    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.OK -> {
+                    resId = response.resId
+                }
+                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 {
+                        when (it) {
+                            is PlainText -> it.stringValue
+                            is At -> it.toString()
+                            else -> ""
+                        }
+                    },
+                    resId = resId,
+                    timeSeconds = source.time
+                )
+            )
+
             println(response._miraiContentToString())
         }
     }

+ 5 - 2
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
@@ -289,8 +291,9 @@ internal class GroupImpl(
                 source.startWaitingSequenceId(this)
             }.sendAndExpect()
             if (response is MessageSvc.PbSendMsg.Response.Failed) {
-                when (response.errorCode) {
+                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")
                 }
             }
@@ -352,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}")

+ 51 - 19
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt

@@ -234,36 +234,39 @@ 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 LightApp -> elements.add(
-                    ImMsgBody.Elem(
-                        lightApp = ImMsgBody.LightAppElem(
-                            data = byteArrayOf(1) + content
-                        )
-                    )
-                )
-                is MergedForwardedMessage -> {
+                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,
+                                serviceId = 35, // ok
                                 template1 = byteArrayOf(1) + content
                             )
                         )
                     )
-                    transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) // required
+                    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
+                                //   is MergedForwardedMessage -> 35
                                 else -> error("unsupported RichMessage: ${it::class.simpleName}")
                             },
                             template1 = byteArrayOf(1) + content
@@ -296,8 +299,9 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
                     transformOneMessage(PlainText(" "))
                 }
             }
-            is QuoteReply,
-            is MessageSource,
+            is QuoteReply, // already transformed above
+            is MessageSource, // mirai only
+            is RichMessage, // already transformed above
             -> {
 
             }
@@ -306,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
 }
@@ -402,7 +420,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
                 last = element
                 return@forEach
             } else {
-                if (last is MergedForwardedMessage && element is PlainText) {
+                if (last is LongMessage && element is PlainText) {
                     if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
                         last = element
                         return@forEach
@@ -416,6 +434,15 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
     }
 }
 
+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.")
+}
+
 /*
     if (this.any<QuoteReply>()) {
         var removed = false
@@ -464,7 +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(MergedForwardedMessage(content))
+                    35 -> message.add(
+                        LongMessage(
+                            content,
+                            this.firstIsInstance<ImMsgBody.GeneralFlags>().longTextResid
+                        )
+                    )
                     else -> {
                         @Suppress("DEPRECATION")
                         MiraiLogger.debug {

+ 41 - 48
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt

@@ -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) {

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

@@ -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

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

@@ -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

+ 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,

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

@@ -44,7 +44,7 @@ internal class MultiMsg : ProtoBuf {
     @Serializable
     class MultiMsgApplyUpRsp(
         @ProtoId(1) val result: Int = 0,
-        @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @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,

+ 54 - 34
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt

@@ -12,7 +12,7 @@
 package net.mamoe.mirai.qqandroid.network.protocol.packet.chat
 
 import kotlinx.io.core.ByteReadPacket
-import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
@@ -29,6 +29,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgTransmit
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MultiMsg
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.MiraiPlatformUtils
@@ -44,8 +45,8 @@ internal class MessageValidationData @OptIn(MiraiInternalAPI::class) constructor
 }
 
 @OptIn(MiraiInternalAPI::class)
-internal fun MessageChain.calculateValidationData(
-    bot: Bot
+internal fun MessageChain.calculateValidationDataForGroup(
+    group: Group
 ): MessageValidationData {
     // top_package.akkv#method_42702
     val source: MessageSource by this.orElse { error("internal error: calculateValidationData: cannot find MessageSource, chain=${this._miraiContentToString()}") }
@@ -55,37 +56,39 @@ internal fun MessageChain.calculateValidationData(
     }
 
     val richTextElems = this.toRichTextElems(source is MessageSourceFromSendGroup)
+        .filterNot { it.generalFlags != null }
 
     val msgTransmit = MsgTransmit.PbMultiMsgTransmit(
         msg = listOf(
             MsgComm.Msg(
                 msgHead = MsgComm.MsgHead(
-                    fromUin = source.senderId,
+                    fromUin = group.bot.uin,
                     msgSeq = source.sequenceId,
                     msgTime = source.time.toInt(),
-                    msgUid = source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT
+                    msgUid = 0x01000000000000000L or source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT
                     mutiltransHead = MsgComm.MutilTransHead(
                         status = 0,
                         msgId = 1
                     ),
                     msgType = 82, // troop
                     groupInfo = MsgComm.GroupInfo(
-                        groupCode = source.toUin,
-                        groupCard = bot.nick,
+                        groupCode = group.id,
+                        groupCard = "Cinnamon"// group.botAsMember.nameCard, // Cinnamon
                     ),
+                    isSrcMsg = false
                 ),
                 msgBody = ImMsgBody.MsgBody(
                     richText = ImMsgBody.RichText(
-                        elems = richTextElems
+                        elems = richTextElems.toMutableList()
                     )
-                )
-            )
+                ),
+            ),
         )
     )
 
     val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
 
-    return MessageValidationData(MiraiPlatformUtils.zip(bytes))
+    return MessageValidationData(MiraiPlatformUtils.gzip(bytes))
 }
 
 /*
@@ -154,18 +157,27 @@ Packet 20:02:50 : =======================共有 0 个包=======================
 internal class MultiMsg {
 
     object ApplyUp : OutgoingPacketFactory<ApplyUp.Response>("MultiMsg.ApplyUp") {
-        class Response(
-            val proto: MultiMsg.MultiMsgApplyUpRsp
-        ) : Packet
+        sealed class Response : Packet {
+            data class RequireUpload(
+                val proto: MultiMsg.MultiMsgApplyUpRsp
+            ) : Response() {
+                override fun toString(): String {
+                    if (PacketLogger.isEnabled) {
+                        return _miraiContentToString()
+                    }
+                    return "MultiMsg.ApplyUp.Response.RequireUpload(proto=$proto)"
+                }
+            }
 
-        fun createForLongMessage(
-            client: QQAndroidClient,
-            message: MessageChain,
-            dstUin: Long, // group uin
-        ): OutgoingPacket = createForLongMessage(client, message.calculateValidationData(client.bot), dstUin)
+            object MessageTooLarge : Response()
+
+            data class OK(
+                val resId: String
+            ) : Response()
+        }
 
         // captured from group
-        private fun createForLongMessage(
+        fun createForLongMessage(
             client: QQAndroidClient,
             messageData: MessageValidationData,
             dstUin: Long // group uin
@@ -173,21 +185,24 @@ internal class MultiMsg {
             writeProtoBuf(
                 MultiMsg.ReqBody.serializer(),
                 MultiMsg.ReqBody(
-                    subcmd = 1,
-                    termType = 5,
-                    platformType = 9,
-                    netType = 3, // wifi=3, wap=5
-                    buildVer = client.buildVer,
                     buType = 1,
+                    buildVer = "8.2.0.1296",
                     multimsgApplyupReq = listOf(
                         MultiMsg.MultiMsgApplyUpReq(
                             applyId = 0,
                             dstUin = dstUin,
                             msgMd5 = messageData.md5,
-                            msgSize = messageData.data.size.toLong(),
+                            msgSize = messageData.data.size.toLong().also {
+                                println("data.size = $it")
+                            },
                             msgType = 3 // TODO 3 for group?
-                        )
-                    )
+                        ),
+                    ),
+                    netType = 3, // wifi=3, wap=5
+                    platformType = 9,
+                    subcmd = 1,
+                    termType = 5,
+                    reqChannelType = 0,
                 )
             )
         }
@@ -210,13 +225,18 @@ internal class MultiMsg {
     }
          */
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
-            val response = readProtoBuf(MultiMsg.MultiMsgApplyUpRsp.serializer())
-            check(response.result == 0) {
-                kotlin.run {
-                    println(response._miraiContentToString())
-                }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" }
+            val body = readProtoBuf(MultiMsg.RspBody.serializer())
+            val response = body.multimsgApplyupRsp!!.first()
+            return when (response.result) {
+                0 -> Response.RequireUpload(response)
+                193 -> Response.MessageTooLarge
+                //1 -> Response.OK(resId = response.msgResid)
+                else -> {
+                    error(kotlin.run {
+                        println(response._miraiContentToString())
+                    }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" })
+                }
             }
-            return Response(response)
         }
     }
 }

+ 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())

+ 29 - 19
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
@@ -46,25 +47,33 @@ interface RichMessage : MessageContent {
          *
          * @param brief 消息内容纯文本, 显示在图片的前面
          */
+        @SinceMirai("0.31.0")
+        @OptIn(MiraiInternalAPI::class)
         @MiraiExperimentalAPI
-        fun longMessage(brief: String, resId: String, time: Long): XmlMessage {
+        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="$brief"
-     m_resid="$resId"
-     m_fileName="$time" sourceMsgId="0" url=""
-     flag="3" adverSign="0" multiMsgFlag="1">
-    <item layout="1">
-        <title>$brief…</title>
-        <hr hidden="false" style="0"/>
-        <summary>点击查看完整消息</summary>
-    </item>
-    <source name="聊天记录" icon="" action="" appid="-1"/>
-</msg>
-            """
-
-            return XmlMessage(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
@@ -142,11 +151,12 @@ class XmlMessage constructor(override val content: String) : RichMessage {
 }
 
 /**
- * 合并转发消息
+ * 消息
  */
 @SinceMirai("0.31.0")
 @MiraiExperimentalAPI
-class MergedForwardedMessage(override val content: String) : RichMessage {
+@MiraiInternalAPI
+class LongMessage(override val content: String, val resId: String) : RichMessage {
     companion object Key : Message.Key<XmlMessage>
 
     // serviceId = 35

+ 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)

+ 4 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt

@@ -18,6 +18,7 @@ import kotlin.jvm.JvmName
 /**
  * 要求 [this] 最小为 [min].
  */
+@PublishedApi
 internal fun Int.coerceAtLeastOrFail(min: Int): Int {
     require(this >= min)
     return this
@@ -26,6 +27,7 @@ internal fun Int.coerceAtLeastOrFail(min: Int): Int {
 /**
  * 要求 [this] 最小为 [min].
  */
+@PublishedApi
 internal fun Long.coerceAtLeastOrFail(min: Long): Long {
     require(this >= min)
     return this
@@ -34,10 +36,12 @@ internal fun Long.coerceAtLeastOrFail(min: Long): Long {
 /**
  * 要求 [this] 最大为 [max].
  */
+@PublishedApi
 internal fun Int.coerceAtMostOrFail(max: Int): Int =
     if (this >= max) error("value is greater than its expected maximum value $max")
     else this
 
+@PublishedApi
 internal fun Long.coerceAtMostOrFail(max: Long): Long =
     if (this >= max) error("value is greater than its expected maximum value $max")
     else this

+ 1 - 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