From 28149dfed2f42e0d1f41edaefe74b209192d0fa7 Mon Sep 17 00:00:00 2001
From: Him188 <Him188@mamoe.net>
Date: Mon, 28 Oct 2019 22:01:49 +0800
Subject: [PATCH] Combine image packets into the same file

---
 .../tim/packet/action/UploadFriendImage.kt    | 350 ---------------
 .../tim/packet/action/UploadGroupImage.kt     | 331 --------------
 .../protocol/tim/packet/action/UploadImage.kt | 424 ++++++++++++++++++
 3 files changed, 424 insertions(+), 681 deletions(-)
 delete mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadFriendImage.kt
 delete mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadGroupImage.kt
 create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt

diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadFriendImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadFriendImage.kt
deleted file mode 100644
index ce2e8817a..000000000
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadFriendImage.kt
+++ /dev/null
@@ -1,350 +0,0 @@
-@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused")
-
-package net.mamoe.mirai.network.protocol.tim.packet.action
-
-import kotlinx.io.core.*
-import net.mamoe.mirai.contact.QQ
-import net.mamoe.mirai.message.ImageId
-import net.mamoe.mirai.network.protocol.tim.TIMProtocol
-import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
-import net.mamoe.mirai.network.protocol.tim.packet.PacketId
-import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
-import net.mamoe.mirai.network.protocol.tim.packet.ResponsePacket
-import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket.Response.State.*
-import net.mamoe.mirai.network.qqAccount
-import net.mamoe.mirai.qqAccount
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.httpPostFriendImage
-import net.mamoe.mirai.utils.io.*
-import net.mamoe.mirai.utils.readUnsignedVarInt
-import net.mamoe.mirai.utils.writeUVarInt
-import net.mamoe.mirai.withSession
-
-/**
- * 上传图片
- * 挂起直到上传完成或失败
- *
- * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
- *
- * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
- */
-suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {
-    FriendImageIdRequestPacket(qqAccount, sessionKey, id, image)
-        .sendAndExpect<FriendImageIdRequestPacket.Response, ImageId> {
-            when (it.state) {
-                REQUIRE_UPLOAD -> {
-                    require(
-                        httpPostFriendImage(
-                            botAccount = bot.qqAccount,
-                            uKeyHex = it.uKey!!.toUHexString(""),
-                            imageInput = image.input,
-                            inputSize = image.inputSize
-                        )
-                    )
-                }
-
-                ALREADY_EXISTS -> {
-
-                }
-
-                OVER_FILE_SIZE_MAX -> {
-                    throw OverFileSizeMaxException()
-                }
-            }
-
-            it.imageId!!
-        }.await()
-}
-
-//fixVer2=00 00 00 01 2E 01 00 00 69 35
-//01 [3E 03 3F A2] [76 E4 B8 DD] 00 00 50 7A 00 0A 00 01 00 01    00 2D 55 73 65 72 44 61 74 61 49 6D 61 67 65 3A 43 32 43 5C 48 31 30 50 60 35 29 24 52 7D 57 45 56 54 41 4B 52 24 45 4E 54 45 58 2E 70 6E 67
-// 00 00 00 F2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01
-
-//01 3E 03 3F A2 76 E4 B8 DD     00 00 50 7B 00 0A 00 01 00 01    00 5E 4F 53 52 6F 6F 74 3A 43 3A 5C 55 73 65 72 73 5C 48 69 6D 31 38 5C 44 6F 63 75 6D 65 6E 74 73 5C 54 65 6E 63 65 6E 74 20 46 69 6C 65 73 5C 31 30 34 30 34 30 30 32 39 30 5C 49 6D 61 67 65 5C 43 32 43 5C 4E 41 4B 60 52 52 4E 24 49 24 24 4B 44 24 34 5B 5B 45 4E 24 4D 4A 30 2E 6A 70 67
-// 00 00 06 99 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01
-
-//01 3E 03 3F A2 76 E4 B8 DD     00 00 50 7C 00 0A 00 01 00 01    00 2D 55 73 65 72 44 61 74 61 49 6D 61 67 65 3A 43 32 43 5C 40 53 51 25 4F 46 43 50 36 4C 48 30 47 34 43 47 57 53 49 52 46 37 32 2E 70 6E 67
-// 00 01 61 A7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01
-/**
- * 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
- */
-@PacketId(0X01_BDu)
-@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
-class SubmitImageFilenamePacket(
-    private val bot: UInt,
-    private val target: UInt,
-    private val filename: String,
-    private val sessionKey: ByteArray
-) : OutgoingPacket() {
-    override fun encode(builder: BytePacketBuilder) = with(builder) {
-        writeQQ(bot)
-        writeHex(TIMProtocol.fixVer2)//?
-        //writeHex("04 00 00 00 01 2E 01 00 00 69 35")
-
-        encryptAndWrite(sessionKey) {
-            writeByte(0x01)
-            writeQQ(bot)
-            writeQQ(target)
-            writeZero(2)
-            writeUByte(0x02u)
-            writeRandom(1)
-            writeHex("00 0A 00 01 00 01")
-            val name = "UserDataImage:$filename"
-            writeShort(name.length.toShort())
-            writeStringUtf8(name)
-            writeHex("00 00")
-            writeRandom(2)//这个也与是哪个好友有关?
-            writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01")//35  02? 最后这个值是与是哪个好友有关
-
-            //this.debugPrintThis("SubmitImageFilenamePacket")
-        }
-
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1A 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1B 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-    }
-
-    @PacketId(0x01_BDu)
-    @PacketVersion(date = "2019.10.19", timVersion = "2.3.2.21173")
-    class Response(input: ByteReadPacket) : ResponsePacket(input) {
-        override fun decode() = with(input) {
-            require(readBytes().contentEquals(expecting))
-        }
-
-        companion object {
-            private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
-        }
-    }
-}
-
-
-/**
- * 请求上传图片. 将发送图片的 md5, size, width, height.
- * 服务器返回以下之一:
- * - 服务器已经存有这个图片
- * - 服务器未存有, 返回一个 key 用于客户端上传
- */
-@PacketId(0x03_52u)
-@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
-class FriendImageIdRequestPacket(
-    private val bot: UInt,
-    private val sessionKey: ByteArray,
-    private val target: UInt,
-    private val image: ExternalImage
-) : OutgoingPacket() {
-
-    //00 00 00 07 00 00 00 4B 08 01 12 03 98 01 01 08 01 12 47 08 A2 FF 8C F0 03 10 89 FC A6 8C 0B 18 00 22 10 2B 23 D7 05 CA D1 F2 CF 37 10 FE 58 26 92 FC C4 28 FD 08 32 1A 7B 00 47 00 47 00 42 00 7E 00 49 00 31 00 5A 00 4D 00 43 00 28 00 25 00 49 00 38 01 48 00 70 42 78 42
-
-    override fun encode(builder: BytePacketBuilder) = with(builder) {
-        writeQQ(bot)
-        //04 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 00 00 00
-        writeHex("04 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 00 00 00")
-
-        encryptAndWrite(sessionKey) {
-            //好友图片
-            // 00 00 00
-            // 07 00
-            // 00 00
-
-            // proto
-
-            // [4D 08]后文长度
-            // 01 12
-            // 03 98
-            // 01 01
-            // 08 01
-            // 12 49
-            // 08 [A2 FF 8C F0 03](1040400290 varint)
-            // 10 [DD F1 92 B7 07](1994701021 varint)
-            // 18 00
-            // 22 [10](=16) [E9 BA 47 2E 36 ED D4 BF 8C 4F E5 6A CB A0 2D 5E](md5)
-            // 28 [CE 0E](1870 varint)
-            // 32 1A
-            // 39 00
-            // 51 00
-            // 24 00
-            // 32 00
-            // 4A 00
-            // 53 00
-            // 25 00
-            // 4C 00
-            // 56 00
-            // 42 00
-            // 33 00
-            // 44 00
-            // 44 00
-            // 38 01
-            // 48 00
-            // 70 [92 03](402 varint)
-            // 78 [E3 01](227 varint)
-
-            //好友图片
-            /*
-             * 00 00 00 07 00 00 00
-             * [4E 08]后文长度
-             * 01 12
-             * 03 98
-             * 01 01
-             * 08 01
-             * 12 4A
-             * 08 [A2 FF 8C F0 03](varint)
-             * 10 [DD F1 92 B7 07](varint)
-             * 18 00//24
-             * 22 10 72 02 57 44 84 1D 83 FC C0 85 A1 E9 10 AA 9C 2C
-             * 28 [BD D9 19](421053 varint)
-             * 32 1A//48
-             * 49 00
-             * 49 00
-             * 25 00
-             * 45 00
-             * 5D 00
-             * 50 00
-             * 41 00
-             * 7D 00
-             * 4F 00
-             * 56 00
-             * 46 00
-             * 4B 00
-             * 5D 00
-             * 38 01
-             * 48 00//78
-             *
-             *
-             * 70 [80 14]
-             * 78 [A0 0B]//84
-             */
-            writeHex("00 00 00 07 00 00 00")
-
-            //proto
-            writeUVarintLVPacket(lengthOffset = { it - 7 }) {
-                writeUByte(0x08u)
-                writeUShort(0x01_12u)
-                writeUShort(0x03_98u)
-                writeUShort(0x01_01u)
-                writeUShort(0x08_01u)
-
-
-                writeUVarintLVPacket(tag = 0x12u, lengthOffset = { it + 1 }) {
-                    writeUByte(0x08u)
-                    writeUVarInt(bot)
-
-                    writeUByte(0x10u)
-                    writeUVarInt(target)
-
-                    writeUShort(0x18_00u)
-
-                    writeUByte(0x22u)
-                    writeUByte(0x10u)
-                    writeFully(image.md5)
-
-                    writeUByte(0x28u)
-                    writeUVarInt(image.inputSize.toUInt())
-
-
-                    writeUByte(0x32u)
-                    //长度应为1A
-                    writeUVarintLVPacket {
-                        writeUShort(0x28_00u)
-                        writeUShort(0x46_00u)
-                        writeUShort(0x51_00u)
-                        writeUShort(0x56_00u)
-                        writeUShort(0x4B_00u)
-                        writeUShort(0x41_00u)
-                        writeUShort(0x49_00u)
-                        writeUShort(0x25_00u)
-                        writeUShort(0x4B_00u)
-                        writeUShort(0x24_00u)
-                        writeUShort(0x55_00u)
-                        writeUShort(0x30_00u)
-                        writeUShort(0x24_00u)
-                    }
-
-                    writeUShort(0x38_01u)
-                    writeUShort(0x48_00u)
-
-                    writeUByte(0x70u)
-                    writeUVarInt(image.width.toUInt())
-                    writeUByte(0x78u)
-                    writeUVarInt(image.height.toUInt())
-                }
-            }
-
-
-            //println(this.build().readBytes().toUHexString())
-        }
-    }
-
-    @PacketId(0x0352u)
-    @PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
-    class Response(input: ByteReadPacket) : ResponsePacket(input) {
-        /**
-         * 访问 HTTP API 时需要使用的一个 key. 128 位
-         */
-        var uKey: ByteArray? = null
-
-        /**
-         * 发送消息时使用的 id
-         */
-        var imageId: ImageId? = null
-
-        lateinit var state: State
-
-        enum class State {
-            /**
-             * 需要上传. 此时 [uKey], [imageId] 均不为 `null`
-             */
-            REQUIRE_UPLOAD,
-            /**
-             * 服务器已有这个图片. 此时 [uKey] 为 `null`, [imageId] 不为 `null`
-             */
-            ALREADY_EXISTS,
-            /**
-             * 图片过大. 此时 [uKey], [imageId] 均为 `null`
-             */
-            OVER_FILE_SIZE_MAX,
-        }
-
-        override fun decode() = with(input) {
-            //00 00 00 08 00 00
-            //01 0D 12 06 98 01 01 A0 01 00 08 01 12 86 02 08 00 10 AB A7 89 D8 02 18 00 28 00 38 B4 C7 E6 B0 02 38 F1 C0 A1 BF 05 38 FB AE FA 95 0A 38 E5 C6 BF EC 06 40 B0 6D 40 90 3F 40 50 40 BB 03 4A 80 01 B3 90 73 32 C0 5D 72 4B 18 AC 16 8A 23 92 21 3C E6 FD 51 33 CE C4 84 1C 4C 7B A2 E5 27 65 1C 99 EE D6 D4 D2 0D B8 10 2D 88 7E 13 71 75 09 36 46 0F BA 87 B3 EA 54 B2 2B 18 8F F3 5A 9D 55 C6 3B E4 CB 9E B3 69 79 E2 51 61 98 1B 04 49 76 58 29 75 E3 73 56 4B 89 A4 54 A2 E1 0C 17 72 8D 77 EA CD CF 9E 68 B7 01 65 7B F1 E3 B7 FC 04 0C F4 D8 8D B3 51 1B B2 4C 14 59 DE FA 0D 64 BD 50 2E ED 52 25 2F 62 63 66 38 63 39 39 65 2D 30 65 63 35 2D 34 33 33 31 2D 62 37 30 61 2D 31 36 33 35 32 66 66 64 38 33 33 33 5A 25 2F 62 63 66 38 63 39 39 65 2D 30 65 63 35 2D 34 33 33 31 2D 62 37 30 61 2D 31 36 33 35 32 66 66 64 38 33 33 33 60 00 68 80 80 08 20 01
-
-            //00 00 00 08 00 00 01 0C 12 06 98 01 01 A0 01 00 08 01 12 85 02 08 00 10 AB A7 89 D8 02 18 00 28 00 38 B4 C7 E6 B0 02 38 B7 87 AC E7 0B 38 FB AE FA 95 0A 38 E5 C6 BF EC 06 40 50 40 90 3F 40 BB 03 40 50 4A 80 01 F2 65 BC F3 E8 C6 F3 30 B1 85 72 86 C0 95 C0 A7 09 E3 84 AC A6 68 C3 AF BB A8 96 64 AA 18 92 96 F7 3C 7B F8 EA 03 C6 6A AD B7 94 BC 76 D4 36 84 25 76 CB DF 5B 7C E7 40 DF 5D FD DF 3D 93 23 96 5D 23 A8 B2 93 FA 21 BF 68 3E 0B 71 D2 9C FF F2 55 45 11 E2 23 2E D0 49 6E 4F 1F DB 18 28 22 68 45 C9 9E A7 F4 AD EF 20 93 55 EB 0E A3 33 7B 18 E8 7C 15 6F 19 26 2C 41 E9 E4 51 61 48 AA 2F EE 52 25 2F 65 39 61 63 62 63 65 39 2D 61 62 39 36 2D 34 30 30 66 2D 38 61 66 30 2D 32 63 34 64 39 37 31 31 32 33 36 62 5A 25 2F 65 39 61 63 62 63 65 39 2D 61 62 39 36 2D 34 30 30 66 2D 38 61 66 30 2D 32 63 34 64 39 37 31 31 32 33 36 62 60 00 68 80 80 08 20 01
-            //00 00 00 08 00 00 01 0D 12 06 98 01 01 A0 01 00 08 01 12 86 02 08 00 10 AB A7 89 D8 02 18 00 28 00 38 B4 C7 E6 B0 02 38 BB C8 E4 E2 0F 38 FB AE FA 9D 0A 38 E5 C6 BF EC 06 40 B0 6D 40 90 3F 40 50 40 BB 03 4A 80 01 0E 26 8D 39 E7 88 22 74 EC 88 2B 04 C5 D1 3D D2 09 A4 2E 48 22 F5 91 51 D5 82 7A 43 9F 45 70 77 79 83 21 87 4E AA 63 6E 73 D5 D3 DA 5F FC 36 BA 97 31 74 49 D9 97 83 58 74 06 BE F2 00 83 CC B9 50 D0 C4 D1 63 33 5F AE EA 1C 99 2D 0D E7 A2 94 97 6E 18 92 86 2C C0 36 E9 D9 E3 82 01 A3 B9 AC F1 90 67 73 F3 3C 0B 26 4C C4 DE 20 AF 3D B3 20 F8 50 B4 0E 78 0E 0E 1E 8C 56 02 21 10 5B 61 39 52 25 2F 31 38 37 31 34 66 66 39 2D 61 30 39 39 2D 34 61 38 64 2D 38 34 39 62 2D 38 37 35 65 65 30 36 65 34 64 32 36 5A 25 2F 31 38 37 31 34 66 66 39 2D 61 30 39 39 2D 34 61 38 64 2D 38 34 39 62 2D 38 37 35 65 65 30 36 65 34 64 32 36 60 00 68 80 80 08 20 01
-            discardExact(6)
-            if (readUByte() != UByte.MIN_VALUE) {
-                //服务器还没有这个图片
-
-                //00 00 00 08 00 00 01 0D 12 06 98 01 01 A0 01 00 08 01 12 86 02 08 00 10 AB A7 89 D8 02 18 00 28 00 38 B4 C7 E6 B0 02 38 F1 C0 A1 BF 05 38 FB AE FA 95 0A 38 E5 C6 BF EC 06 40 B0 6D 40 90 3F 40 50 40 BB 03
-                // 4A [80 01] B5 29 1A 1B 0E 63 79 8B 34 B1 4E 2A 2A 9E 69 09 A7 69 F5 C6 4F 95 DA 96 A9 1B E3 CD 6F 3D 30 EE 59 C0 30 22 BF F0 2D 88 2D A7 6C B2 09 AD D6 CE E1 46 84 FC 7D 19 AF 1A 37 91 98 AD 2C 45 25 AA 17 2F 81 DC 5A 7F 30 F4 2D 73 E5 1C 8B 8A 23 85 42 9D 8D 5C 18 15 32 D1 CA A3 4D 01 7C 59 11 73 DA B6 09 C2 6D 58 35 EF 48 88 44 0F 2D 17 09 52 DF D4 EA A7 85 2F 27 CE DF A8 F5 9B CD C9 84 C2 // 52 [25] 2F 30 31 65 65 36 34 32 36 2D 35 66 66 31 2D 34 63 66 30 2D 38 32 37 38 2D 65 38 36 33 34 64 32 39 30 39 65 66 5A 25 2F 30 31 65 65 36 34 32 36 2D 35 66 66 31 2D 34 63 66 30 2D 38 32 37 38 2D 65 38 36 33 34 64 32 39 30 39 65 66 60 00 68 80 80 08 20 01
-
-                discardExact(60)
-
-                discardExact(1)//4A, id
-                uKey = readBytes(readUnsignedVarInt().toInt())//128
-
-                discardExact(1)//52, id
-                imageId = ImageId(readString(readUnsignedVarInt().toInt()))//37
-                state = State.REQUIRE_UPLOAD
-
-                //DebugLogger.logPurple("获得 uKey(${uKey!!.size})=${uKey!!.toUHexString()}")
-                //DebugLogger.logPurple("获得 imageId(${imageId!!.value.length})=${imageId}")
-            } else {
-                //服务器已经有这个图片了
-                //DebugLogger.logPurple("服务器已有好友图片 ")
-                // 89
-                // 12 06 98 01 01 A0 01 00 08 01 12 82 01 08 00 10 AB A7 89 D8 02 18 00 28 01 32 20 0A 10 5A 39 37 10 EA D5 B5 57 A8 04 14 70 CE 90 67 14 10 67 18 8A 94 17 20 ED 03 28 97 04 30 0A 52 25 2F 39 38 31 65 61 31 64 65 2D 62 32 31 33 2D 34 31 61 39 2D 38 38 37 65 2D 32 38 37 39 39 66 31 39 36 37 35 65 5A 25 2F 39 38 31 65 61 31 64 65 2D 62 32 31 33 2D 34 31 61 39 2D 38 38 37 65 2D 32 38 37 39 39 66 31 39 36 37 35 65 60 00 68 80 80 08 20 01
-
-
-                //83 12 06 98 01 01 A0 01 00 08 01 12 7D 08 00 10 9B A4 DC 92 06 18 00 28 01 32 1B 0A 10 8E C4 9D 72 26 AE 20 C0 5D A2 B6 78 4D 12 B7 3A 10 00 18 86 1F 20 30 28 30 52 25 2F 30 31 62
-                val toDiscard = readUByte().toInt() - 37
-                if (toDiscard < 0) {
-                    state = OVER_FILE_SIZE_MAX
-                } else {
-                    discardExact(toDiscard)
-                    imageId = ImageId(readString(37))
-                    state = ALREADY_EXISTS
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadGroupImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadGroupImage.kt
deleted file mode 100644
index dd266b43b..000000000
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadGroupImage.kt
+++ /dev/null
@@ -1,331 +0,0 @@
-@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
-
-package net.mamoe.mirai.network.protocol.tim.packet.action
-
-import kotlinx.coroutines.withContext
-import kotlinx.io.core.*
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.GroupId
-import net.mamoe.mirai.contact.GroupInternalId
-import net.mamoe.mirai.contact.withSession
-import net.mamoe.mirai.message.ImageId
-import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
-import net.mamoe.mirai.network.protocol.tim.packet.PacketId
-import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
-import net.mamoe.mirai.network.protocol.tim.packet.ResponsePacket
-import net.mamoe.mirai.qqAccount
-import net.mamoe.mirai.utils.ExternalImage
-import net.mamoe.mirai.utils.httpPostGroupImage
-import net.mamoe.mirai.utils.io.*
-import kotlin.coroutines.coroutineContext
-
-
-/**
- * 图片文件过大
- */
-class OverFileSizeMaxException : IllegalStateException()
-
-/**
- * 上传群图片
- * 挂起直到上传完成或失败
- *
- * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
- *
- * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
- */
-suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
-    val userContext = coroutineContext
-    GroupImageIdRequestPacket(bot.qqAccount, internalId, image, sessionKey)
-        .sendAndExpect<GroupImageIdRequestPacket.Response, Unit> {
-            withContext(userContext) {
-                when (it.state) {
-                    GroupImageIdRequestPacket.Response.State.REQUIRE_UPLOAD -> {
-                        httpPostGroupImage(
-                            botAccount = bot.qqAccount,
-                            groupId = GroupId(id),
-                            imageInput = image.input,
-                            inputSize = image.inputSize,
-                            uKeyHex = it.uKey!!.toUHexString("")
-                        )
-                    }
-
-                    GroupImageIdRequestPacket.Response.State.ALREADY_EXISTS -> {
-
-                    }
-
-                    GroupImageIdRequestPacket.Response.State.OVER_FILE_SIZE_MAX -> throw OverFileSizeMaxException()
-                }
-            }
-        }.join()
-    image.groupImageId
-}
-
-/**
- * 获取 Image Id 和上传用的一个 uKey
- */
-@PacketId(0x0388u)
-@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
-class GroupImageIdRequestPacket(
-    private val bot: UInt,
-    private val groupInternalId: GroupInternalId,
-    private val image: ExternalImage,
-    private val sessionKey: ByteArray
-) : OutgoingPacket() {
-
-    override fun encode(builder: BytePacketBuilder) = with(builder) {
-        //未知图片A
-        // 00 00 00 07 00 00 00
-        // 53 08 =后文长度-6
-        // 01 12 03 98 01 02 10 02 22 4F 08 F3 DB F3 E3 01 10 A2 FF 8C F0 03 18 B1 C7 B1 BB 0A 22 10 77 FB 3D 6F 97 BD 7B F0 C4 1F DC 60 1F 22 D2 7C 28 04 30 02 38 20 40 FF 01 48 00 50 01 5A 05 32 36 39 33 33 60 00 68 00 70 00 78 00 80 01 A4 05 88 01 D8 03 90 01 EB 07 A0 01 01
-
-        //小图B
-        // 00 00 00 07 00 00 00
-        // 5B =后文长度-7
-        // 08 01 12 03 98 01 01 10 01 1A
-        // 57长度
-        // 08 FB D2 D8 94 02
-        // 10 A2 FF 8C F0 03
-        // 18 00
-        // 22 [10] 7A A4 B3 AA 8C 3C 0F 45 2D 9B 7F 30 2A 0A CE AA
-        // 28 F3 06//size
-        // 32 1A
-        // 29 00
-        // 37 00
-        // 42 00
-        // 53 00
-        // 4B 00
-        // 48 00
-        // 32 00
-        // 44 00
-        // 35 00
-        // 54 00
-        // 51 00
-        // 28 00
-        // 5A 00
-        // 38 01
-        // 48 01
-        // 50 41 //宽度
-        // 58 34 //高度
-        // 60 04
-        // 6A [05] 32 36 39 33 33
-        // 70 00
-        // 78 03
-        // 80 01 00
-
-        //450*298
-        //00 00 00 07 00 00 00
-        // 5D=后文-7 varint
-        // 08 01 12 03 98 01 01 10 01 1A
-        // 59 =后文长度 varint
-        // 08 A0 89 F7 B6 03
-        // 10 A2 FF 8C F0 03
-        // 18 00
-        // 22 10  01 FC 9D 6B E9 B2 D9 CD AC 25 66 73 F9 AF 6A 67
-        // 28 [C9 10] varint size
-        // 32 1A
-        // 58 00 51 00 56 00 51 00 58 00 47 00 55 00 47 00 38 00 57 00 5F 00 4A 00 43 00
-        // 38 01 48 01
-        // 50 [C2 03]
-        // 58 [AA 02]
-        // 60 02
-        // 6A 05 32 36 39 33 33
-        // 70 00
-        // 78 03
-        // 80 01
-        // 00
-
-        //大图C
-        // 00 00 00 07 00 00 00
-        // 5E 08 =后文长度-6
-        // 01 12 03 98 01 01 10 01 1A
-        // 5A长度
-        // 08 A0 89 F7 B6 03
-        // 10 A2 FF 8C F0 03
-        // 18 00
-        // 22 [10] F1 DD 65 4D A1 AB 66 B4 0F B5 27 B5 14 8E 73 B5
-        // 28 96 83 08//size
-        // 32 1A
-        // 31 00
-        // 35 00
-        // 4C 00
-        // 24 00
-        // 40 00
-        // 5B 00
-        // 4D 00
-        // 5B 00
-        // 39 00
-        // 39 00
-        // 40 00
-        // 57 00
-        // 5D 00
-        // 38 01
-        // 48 01
-        // 50 80 14 //宽度
-        // 58 A0 0B //高度
-        // 60 02
-        // 6A [05] 32 36 39 33 33
-        // 70 00
-        // 78 03
-        // 80 01 00
-
-
-        //00 00 00 07 00 00 00
-        // 5B 08 01 12 03 98 01 01 10 01 1A
-        // 57
-        // 08 A0 89 F7 B6 03
-        // 10 A2 FF 8C F0 03
-        // 18 00
-        // 22 10 39 F7 65 32 E1 AB 5C A7 86 D7 A5 13 89 22 53 85
-        // 28 90 23
-        // 32 1A
-        // 28 00 52 00 49 00 5F 00 36 00 31 00 28 00 32 00 52 00 59 00 4B 00 59 00 43 00
-        // 38 01
-        // 48 01
-        // 50 2D
-        // 58 2D
-        // 60 03
-        // 6A 05 32 36 39 33 33
-        // 70 00
-        // 78 03
-        // 80 01 00
-        writeQQ(bot)
-        writeHex("04 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 00 00 00")
-        //writeHex(TIMProtocol.version0x02)
-
-        encryptAndWrite(sessionKey) {
-            writeHex("00 00 00 07 00 00 00")
-
-            writeUVarintLVPacket(lengthOffset = { it - 7 }) {
-                writeByte(0x08)
-                writeHex("01 12 03 98 01 01 10 01 1A")
-
-                writeUVarintLVPacket(lengthOffset = { it }) {
-                    writeTUVarint(0x08u, groupInternalId.value)
-                    writeTUVarint(0x10u, bot)
-                    writeTV(0x1800u)
-
-                    writeUByte(0x22u)
-                    writeUByte(0x10u)
-                    writeFully(image.md5)
-
-                    writeTUVarint(0x28u, image.inputSize.toUInt())
-                    writeUVarintLVPacket(tag = 0x32u) {
-                        writeTV(0x5B_00u)
-                        writeTV(0x40_00u)
-                        writeTV(0x33_00u)
-                        writeTV(0x48_00u)
-                        writeTV(0x5F_00u)
-                        writeTV(0x58_00u)
-                        writeTV(0x46_00u)
-                        writeTV(0x51_00u)
-                        writeTV(0x45_00u)
-                        writeTV(0x51_00u)
-                        writeTV(0x40_00u)
-                        writeTV(0x24_00u)
-                        writeTV(0x4F_00u)
-                    }
-                    writeTV(0x38_01u)
-                    writeTV(0x48_01u)
-                    writeTUVarint(0x50u, image.width.toUInt())
-                    writeTUVarint(0x58u, image.height.toUInt())
-                    writeTV(0x60_04u)//这个似乎会变 有时候是02, 有时候是03
-                    writeTByteArray(0x6Au, value0x6A)
-
-                    writeTV(0x70_00u)
-                    writeTV(0x78_03u)
-                    writeTV(0x80_01u)
-                    writeUByte(0u)
-                }
-            }
-
-            /*
-             this.debugColorizedPrintThis(compareTo =  buildPacket {
-                 writeHex("00 00 00 07 00 00 00 5E 08 01 12 03 98 01 01 10 01 1A")
-                 writeHex("5A 08")
-                 writeUVarInt(groupId)
-                 writeUByte(0x10u)
-                 writeUVarInt(bot)
-                 writeHex("18 00 22 10")
-                 writeFully(image.md5)
-                 writeUByte(0x28u)
-                 writeUVarInt(image.fileSize.toUInt())
-                 writeHex("32 1A 37 00 4D 00 32 00 25 00 4C 00 31 00 56 00 32 00 7B 00 39 00 30 00 29 00 52 00")
-                 writeHex("38 01 48 01 50")
-                 writeUVarInt(image.width.toUInt())
-                 writeUByte(0x58u)
-                 writeUVarInt(image.height.toUInt())
-                 writeHex("60 04 6A 05 32 36 36 35 36 70 00 78 03 80 01 00")
-             }.readBytes().toUHexString())
-                */
-        }
-    }
-
-    companion object {
-        private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u)
-    }
-
-    @PacketId(0x0388u)
-    @PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
-    class Response(input: ByteReadPacket) : ResponsePacket(input) {
-        lateinit var state: State
-
-        /**
-         * 访问 HTTP API 时需要使用的一个 key. 128 位
-         */
-        var uKey: ByteArray? = null
-
-        enum class State {
-            /**
-             * 需要上传. 此时 [uKey] 不为 `null`
-             */
-            REQUIRE_UPLOAD,
-            /**
-             * 服务器已有这个图片. 此时 [uKey] 为 `null`
-             */
-            ALREADY_EXISTS,
-            /**
-             * 图片过大. 此时 [uKey] 为 `null`
-             */
-            OVER_FILE_SIZE_MAX,
-        }
-
-        override fun decode(): Unit = with(input) {
-            discardExact(6)//00 00 00 05 00 00
-
-            val length = remaining - 128 - 14
-            if (length < 0) {
-                state = if (readUShort().toUInt() == 0x0025u) {
-                    State.OVER_FILE_SIZE_MAX
-                } else {
-                    State.ALREADY_EXISTS
-                }
-
-                //图片过大 00 25 12 03 98 01 01 08 9B A4 DC 92 06 10 01 1A 1B 08 00 10 C5 01 1A 12 6F 76 65 72 20 66 69 6C 65 20 73 69 7A 65 20 6D 61 78 20 00
-                //图片过大 00 25 12 03 98 01 01 08 9B A4 DC 92 06 10 01 1A 1B 08 00 10 C5 01 1A 12 6F 76 65 72 20 66 69 6C 65 20 73 69 7A 65 20 6D 61 78 20 00
-                //图片已有 00 3F 12 03 98 01 01 08 9B A4 DC 92 06 10 01 1A 35 08 00 10 00 20 01 2A 1F 0A 10 24 66 B9 6B E8 58 FE C0 12 BD 1E EC CB 74 A8 8E 10 04 18 83 E2 AF 01 20 80 3C 28 E0 21 30 EF 9A 88 B9 0B 38 50 48 90 D7 DA B0 08
-                //debugPrint("后文")
-                return@with
-            }
-
-            discardExact(length)
-            uKey = readBytes(128)
-            state = State.REQUIRE_UPLOAD
-            //} else {
-            //    println("服务器已经有了这个图片")
-            //println("后文 = ${readRemainingBytes().toUHexString()}")
-            //}
-
-
-            // 已经有了的一张图片
-            // 00 3B 12 03 98 01 01
-            // 08 AB A7 89 D8 02 //群ID
-            // 10 01 1A 31 08 00 10 00 20 01 2A 1B 0A 10 7A A4 B3 AA 8C 3C 0F 45 2D 9B 7F 30 2A 0A CE AA 10 04 18 F3 06 20 41 28 34 30 DF CF A2 93 02 38 50 48 D0 A9 E5 C8 0B
-
-            // 服务器还没有的一张图片
-            // 02 4E 12 03 98 01 02
-            // 08 AB A7 89 D8 02 //群ID
-            // 10 02 22 C3 04 08 F8 9D D0 F5 09 12 10 2F CA 6B E7 B7 95 B7 27 06 35 27 54 0E 43 B4 30 18 00 48 BD EE 92 8D 05 48 BD EE 92 E5 01 48 BB CA 80 A3 02 48 BA F6 D7 5C 48 EF BC 90 F5 0A 50 50 50 50 50 50 50 50 50 50 5A 0D 67 63 68 61 74 2E 71 70 69 63 2E 63 6E 62 79 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 33 39 36 37 39 34 39 34 32 37 2F 33 39 36 37 39 34 39 34 32 37 2D 32 36 36 32 36 30 30 34 34 30 2D 32 46 43 41 36 42 45 37 42 37 39 35 42 37 32 37 30 36 33 35 32 37 35 34 30 45 34 33 42 34 33 30 2F 31 39 38 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 6A 77 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 33 39 36 37 39 34 39 34 32 37 2F 33 39 36 37 39 34 39 34 32 37 2D 32 36 36 32 36 30 30 34 34 30 2D 32 46 43 41 36 42 45 37 42 37 39 35 42 37 32 37 30 36 33 35 32 37 35 34 30 45 34 33 42 34 33 30 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 72 79 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 33 39 36 37 39 34 39 34 32 37 2F 33 39 36 37 39 34 39 34 32 37 2D 32 36 36 32 36 30 30 34 34 30 2D 32 46 43 41 36 42 45 37 42 37 39 35 42 37 32 37 30 36 33 35 32 37 35 34 30 45 34 33 42 34 33 30 2F 37 32 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 78 00
-            // [80 01] 04 9A 01 79 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 33 39 36 37 39 34 39 34 32 37 2F 33 39 36 37 39 34 39 34 32 37 2D 32 36 36 32 36 30 30 34 34 30 2D 32 46 43 41 36 42 45 37 42 37 39 35 42 37 32 37 30 36 33 35 32 37 35 34 30 45 34 33 42 34 33 30 2F 34 30 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33 A0 01 00
-        }
-    }
-}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt
new file mode 100644
index 000000000..879b68915
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt
@@ -0,0 +1,424 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused")
+
+package net.mamoe.mirai.network.protocol.tim.packet.action
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.post
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.URLProtocol
+import io.ktor.http.userAgent
+import kotlinx.coroutines.withContext
+import kotlinx.io.core.*
+import net.mamoe.mirai.contact.*
+import net.mamoe.mirai.message.ImageId
+import net.mamoe.mirai.network.protocol.tim.TIMProtocol
+import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
+import net.mamoe.mirai.network.protocol.tim.packet.PacketId
+import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
+import net.mamoe.mirai.network.protocol.tim.packet.ResponsePacket
+import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket.Response.State.*
+import net.mamoe.mirai.network.qqAccount
+import net.mamoe.mirai.qqAccount
+import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.io.*
+import net.mamoe.mirai.withSession
+import kotlin.coroutines.coroutineContext
+
+/**
+ * 图片文件过大
+ */
+class OverFileSizeMaxException : IllegalStateException()
+
+/**
+ * 上传群图片
+ * 挂起直到上传完成或失败
+ *
+ * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
+ *
+ * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
+ */
+suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
+    val userContext = coroutineContext
+    GroupImageIdRequestPacket(bot.qqAccount, internalId, image, sessionKey)
+        .sendAndExpect<GroupImageIdRequestPacket.Response, Unit> {
+            withContext(userContext) {
+                when (it.state) {
+                    GroupImageIdRequestPacket.Response.State.REQUIRE_UPLOAD -> httpClient.postImage(
+                        htcmd = "0x6ff0071",
+                        uin = bot.qqAccount,
+                        groupId = GroupId(id),
+                        imageInput = image.input,
+                        inputSize = image.inputSize,
+                        uKeyHex = it.uKey!!.toUHexString("")
+                    )
+
+                    GroupImageIdRequestPacket.Response.State.ALREADY_EXISTS -> {
+                    }
+
+                    GroupImageIdRequestPacket.Response.State.OVER_FILE_SIZE_MAX -> throw OverFileSizeMaxException()
+                }
+            }
+        }.join()
+    image.groupImageId
+}
+
+/**
+ * 上传图片
+ * 挂起直到上传完成或失败
+ *
+ * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
+ *
+ * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
+ */
+suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {
+    FriendImageIdRequestPacket(qqAccount, sessionKey, id, image)
+        .sendAndExpect<FriendImageIdRequestPacket.Response, ImageId> {
+            when (it.state) {
+                REQUIRE_UPLOAD -> httpClient.postImage(
+                    htcmd = "0x6ff0070",
+                    uin = bot.qqAccount,
+                    groupId = null,
+                    uKeyHex = it.uKey!!.toUHexString(""),
+                    imageInput = image.input,
+                    inputSize = image.inputSize
+                )
+
+                ALREADY_EXISTS -> {
+                }
+
+                OVER_FILE_SIZE_MAX -> throw OverFileSizeMaxException()
+            }
+
+            it.imageId!!
+        }.await()
+}
+
+@Suppress("SpellCheckingInspection")
+internal suspend inline fun HttpClient.postImage(
+    htcmd: String,
+    uin: UInt,
+    groupId: GroupId?,
+    imageInput: Input,
+    inputSize: Long,
+    uKeyHex: String
+): Boolean = try {
+    post<HttpStatusCode> {
+        url {
+            protocol = URLProtocol.HTTP
+            host = "htdata2.qq.com"
+            path("cgi-bin/httpconn")
+
+            parameters["htcmd"] = htcmd
+            parameters["uin"] = uin.toLong().toString()
+
+            if (groupId != null) parameters["groupcode"] = groupId.value.toLong().toString()
+
+            parameters["term"] = "pc"
+            parameters["ver"] = "5603"
+            parameters["filesize"] = inputSize.toString()
+            parameters["range"] = 0.toString()
+            parameters["ukey"] = uKeyHex
+
+            userAgent("QQClient")
+        }
+
+        configureBody(inputSize, imageInput)
+    } == HttpStatusCode.OK
+} finally {
+    imageInput.close()
+}
+
+/**
+ * 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
+ */
+@Deprecated("Useless packet")
+@PacketId(0X01_BDu)
+@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
+class SubmitImageFilenamePacket(
+    private val bot: UInt,
+    private val target: UInt,
+    private val filename: String,
+    private val sessionKey: ByteArray
+) : OutgoingPacket() {
+    override fun encode(builder: BytePacketBuilder) = with(builder) {
+        writeQQ(bot)
+        writeHex(TIMProtocol.fixVer2)//?
+        //writeHex("04 00 00 00 01 2E 01 00 00 69 35")
+
+        encryptAndWrite(sessionKey) {
+            writeByte(0x01)
+            writeQQ(bot)
+            writeQQ(target)
+            writeZero(2)
+            writeUByte(0x02u)
+            writeRandom(1)
+            writeHex("00 0A 00 01 00 01")
+            val name = "UserDataImage:$filename"
+            writeShort(name.length.toShort())
+            writeStringUtf8(name)
+            writeHex("00 00")
+            writeRandom(2)//这个也与是哪个好友有关?
+            writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01")//35  02? 最后这个值是与是哪个好友有关
+
+            //this.debugPrintThis("SubmitImageFilenamePacket")
+        }
+
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1A 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1B 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+    }
+
+    @PacketId(0x01_BDu)
+    @PacketVersion(date = "2019.10.19", timVersion = "2.3.2.21173")
+    class Response(input: ByteReadPacket) : ResponsePacket(input) {
+        override fun decode() = with(input) {
+            require(readBytes().contentEquals(expecting))
+        }
+
+        companion object {
+            private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
+        }
+    }
+}
+
+
+/**
+ * 请求上传图片. 将发送图片的 md5, size, width, height.
+ * 服务器返回以下之一:
+ * - 服务器已经存有这个图片
+ * - 服务器未存有, 返回一个 key 用于客户端上传
+ */
+@PacketId(0x03_52u)
+@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
+class FriendImageIdRequestPacket(
+    private val bot: UInt,
+    private val sessionKey: ByteArray,
+    private val target: UInt,
+    private val image: ExternalImage
+) : OutgoingPacket() {
+
+    override fun encode(builder: BytePacketBuilder) = with(builder) {
+        writeQQ(bot)
+        writeHex("04 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 00 00 00")
+
+        encryptAndWrite(sessionKey) {
+            writeHex("00 00 00 07 00 00 00")
+
+            writeUVarintLVPacket(lengthOffset = { it - 7 }) {
+                writeUByte(0x08u)
+                writeUShort(0x01_12u)
+                writeUShort(0x03_98u)
+                writeUShort(0x01_01u)
+                writeUShort(0x08_01u)
+
+                writeUVarintLVPacket(tag = 0x12u, lengthOffset = { it + 1 }) {
+                    writeUByte(0x08u)
+                    writeUVarInt(bot)
+
+                    writeUByte(0x10u)
+                    writeUVarInt(target)
+
+                    writeUShort(0x18_00u)
+
+                    writeUByte(0x22u)
+                    writeUByte(0x10u)
+                    writeFully(image.md5)
+
+                    writeUByte(0x28u)
+                    writeUVarInt(image.inputSize.toUInt())
+
+
+                    writeUByte(0x32u)
+                    //长度应为1A
+                    writeUVarintLVPacket {
+                        writeUShort(0x28_00u)
+                        writeUShort(0x46_00u)
+                        writeUShort(0x51_00u)
+                        writeUShort(0x56_00u)
+                        writeUShort(0x4B_00u)
+                        writeUShort(0x41_00u)
+                        writeUShort(0x49_00u)
+                        writeUShort(0x25_00u)
+                        writeUShort(0x4B_00u)
+                        writeUShort(0x24_00u)
+                        writeUShort(0x55_00u)
+                        writeUShort(0x30_00u)
+                        writeUShort(0x24_00u)
+                    }
+
+                    writeUShort(0x38_01u)
+                    writeUShort(0x48_00u)
+
+                    writeUByte(0x70u)
+                    writeUVarInt(image.width.toUInt())
+                    writeUByte(0x78u)
+                    writeUVarInt(image.height.toUInt())
+                }
+            }
+        }
+    }
+
+    @PacketId(0x0352u)
+    @PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
+    class Response(input: ByteReadPacket) : ResponsePacket(input) {
+        /**
+         * 访问 HTTP API 时需要使用的一个 key. 128 位
+         */
+        var uKey: ByteArray? = null
+
+        /**
+         * 发送消息时使用的 id
+         */
+        var imageId: ImageId? = null
+
+        lateinit var state: State
+
+        enum class State {
+            /**
+             * 需要上传. 此时 [uKey], [imageId] 均不为 `null`
+             */
+            REQUIRE_UPLOAD,
+            /**
+             * 服务器已有这个图片. 此时 [uKey] 为 `null`, [imageId] 不为 `null`
+             */
+            ALREADY_EXISTS,
+            /**
+             * 图片过大. 此时 [uKey], [imageId] 均为 `null`
+             */
+            OVER_FILE_SIZE_MAX,
+        }
+
+        override fun decode() = with(input) {
+            discardExact(6)
+            if (readUByte() != UByte.MIN_VALUE) {
+                discardExact(60)
+
+                discardExact(1)//4A, id
+                uKey = readBytes(readUnsignedVarInt().toInt())//128
+
+                discardExact(1)//52, id
+                imageId = ImageId(readString(readUnsignedVarInt().toInt()))//37
+                state = REQUIRE_UPLOAD
+            } else {
+                val toDiscard = readUByte().toInt() - 37
+                if (toDiscard < 0) {
+                    state = OVER_FILE_SIZE_MAX
+                } else {
+                    discardExact(toDiscard)
+                    imageId = ImageId(readString(37))
+                    state = ALREADY_EXISTS
+                }
+            }
+        }
+    }
+}
+
+
+/**
+ * 获取 Image Id 和上传用的一个 uKey
+ */
+@PacketId(0x0388u)
+@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
+class GroupImageIdRequestPacket(
+    private val bot: UInt,
+    private val groupInternalId: GroupInternalId,
+    private val image: ExternalImage,
+    private val sessionKey: ByteArray
+) : OutgoingPacket() {
+
+    override fun encode(builder: BytePacketBuilder) = with(builder) {
+        writeQQ(bot)
+        writeHex("04 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 00 00 00")
+
+        encryptAndWrite(sessionKey) {
+            writeHex("00 00 00 07 00 00 00")
+
+            writeUVarintLVPacket(lengthOffset = { it - 7 }) {
+                writeByte(0x08)
+                writeHex("01 12 03 98 01 01 10 01 1A")
+
+                writeUVarintLVPacket(lengthOffset = { it }) {
+                    writeTUVarint(0x08u, groupInternalId.value)
+                    writeTUVarint(0x10u, bot)
+                    writeTV(0x1800u)
+
+                    writeUByte(0x22u)
+                    writeUByte(0x10u)
+                    writeFully(image.md5)
+
+                    writeTUVarint(0x28u, image.inputSize.toUInt())
+                    writeUVarintLVPacket(tag = 0x32u) {
+                        writeTV(0x5B_00u)
+                        writeTV(0x40_00u)
+                        writeTV(0x33_00u)
+                        writeTV(0x48_00u)
+                        writeTV(0x5F_00u)
+                        writeTV(0x58_00u)
+                        writeTV(0x46_00u)
+                        writeTV(0x51_00u)
+                        writeTV(0x45_00u)
+                        writeTV(0x51_00u)
+                        writeTV(0x40_00u)
+                        writeTV(0x24_00u)
+                        writeTV(0x4F_00u)
+                    }
+                    writeTV(0x38_01u)
+                    writeTV(0x48_01u)
+                    writeTUVarint(0x50u, image.width.toUInt())
+                    writeTUVarint(0x58u, image.height.toUInt())
+                    writeTV(0x60_04u)//这个似乎会变 有时候是02, 有时候是03
+                    writeTByteArray(0x6Au, value0x6A)
+
+                    writeTV(0x70_00u)
+                    writeTV(0x78_03u)
+                    writeTV(0x80_01u)
+                    writeUByte(0u)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u)
+    }
+
+    @PacketId(0x0388u)
+    @PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
+    class Response(input: ByteReadPacket) : ResponsePacket(input) {
+        lateinit var state: State
+
+        /**
+         * 访问 HTTP API 时需要使用的一个 key. 128 位
+         */
+        var uKey: ByteArray? = null
+
+        enum class State {
+            /**
+             * 需要上传. 此时 [uKey] 不为 `null`
+             */
+            REQUIRE_UPLOAD,
+            /**
+             * 服务器已有这个图片. 此时 [uKey] 为 `null`
+             */
+            ALREADY_EXISTS,
+            /**
+             * 图片过大. 此时 [uKey] 为 `null`
+             */
+            OVER_FILE_SIZE_MAX,
+        }
+
+        override fun decode(): Unit = with(input) {
+            discardExact(6)//00 00 00 05 00 00
+
+            val length = remaining - 128 - 14
+            if (length < 0) {
+                state = if (readUShort().toUInt() == 0x0025u) State.OVER_FILE_SIZE_MAX else State.ALREADY_EXISTS
+                return@with
+            }
+
+            discardExact(length)
+            uKey = readBytes(128)
+            state = State.REQUIRE_UPLOAD
+        }
+    }
+}
\ No newline at end of file