From d87c7f629ab1ec15ac22f1be07fb262a466309c2 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 17 Nov 2019 16:36:29 +0800 Subject: [PATCH] ProtoBuf implementation --- .../action/{UploadImage.kt => FriendImage.kt} | 360 +----------------- .../protocol/tim/packet/action/GroupImage.kt | 224 +++++++++++ .../protocol/tim/packet/action/HttpAPIs.kt | 48 +++ .../tim/packet/action/ImageUploadInfo.kt | 62 +++ .../kotlin/net.mamoe.mirai/utils/Proto.kt | 140 +++++++ .../utils/io/TypeConvertion.kt | 17 +- .../kotlin/net.mamoe.mirai/utils/io/Varint.kt | 36 +- mirai-debug/build.gradle.kts | 42 +- mirai-debug/src/main/kotlin/test/ProtoTest.kt | 57 +++ mirai-demos/mirai-demo-gentleman/build.gradle | 6 +- .../src/main/kotlin/demo/gentleman/Main.kt | 4 +- 11 files changed, 614 insertions(+), 382 deletions(-) rename mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/{UploadImage.kt => FriendImage.kt} (50%) create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt create mode 100644 mirai-debug/src/main/kotlin/test/ProtoTest.kt 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/FriendImage.kt similarity index 50% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/FriendImage.kt index a7cffe76c..ba8945999 100644 --- 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/FriendImage.kt @@ -1,17 +1,11 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH") +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS") package net.mamoe.mirai.network.protocol.tim.packet.action -import io.ktor.client.HttpClient import io.ktor.client.request.get -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.charsets.Charsets import kotlinx.io.core.* -import net.mamoe.mirai.contact.* +import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.message.ImageId import net.mamoe.mirai.message.requireLength import net.mamoe.mirai.network.BotNetworkHandler @@ -22,50 +16,9 @@ import net.mamoe.mirai.network.qqAccount import net.mamoe.mirai.qqAccount import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.Http -import net.mamoe.mirai.utils.assertUnreachable -import net.mamoe.mirai.utils.configureBody 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 - val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect() - - withContext(userContext) { - when (response) { - is GroupImageUKey -> Http.postImage( - htcmd = "0x6ff0071", - uin = bot.qqAccount, - groupId = GroupId(id), - imageInput = image.input, - inputSize = image.inputSize, - uKeyHex = response.uKey.toUHexString("") - ) - - is GroupImageAlreadyExists -> { - } - - is GroupImageOverFileSizeMax -> throw OverFileSizeMaxException() - else -> assertUnreachable() - } - } - - return image.groupImageId -} /** * 上传图片 @@ -97,93 +50,6 @@ suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession { }.await() } -@Suppress("SpellCheckingInspection") -internal suspend inline fun HttpClient.postImage( - htcmd: String, - uin: UInt, - groupId: GroupId?, - imageInput: Input, - inputSize: Long, - uKeyHex: String -): Boolean = try { - post { - 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") -@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME) -@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)") -object SubmitImageFilenamePacket : PacketFactory { - operator fun invoke( - bot: UInt, - target: UInt, - filename: String, - sessionKey: SessionKey - ): OutgoingPacket = buildOutgoingPacket { - writeQQ(bot) - writeFully(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 - } - - @PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)") - class Response { - override fun decode() = with(input) { - require(readBytes().contentEquals(expecting)) - } - - companion object { - private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00) - } - } -}*/ // region FriendImageResponse @@ -224,44 +90,6 @@ object FriendImageOverFileSizeMax : FriendImageResponse { // endregion -// regiion GroupImageResponse -interface GroupImageResponse : EventPacket - -/** - * 图片数据地址. - */ -// TODO: 2019/11/15 应该为 inline class, 但 kotlin 有 bug -data class GroupImageLink(inline val value: String) : GroupImageResponse { - suspend fun downloadAsByteArray(): ByteArray = download().readBytes() - suspend fun download(): ByteReadPacket = Http.get(value) - - override fun toString(): String = "GroupImageLink($value)" -} - -/** - * 访问 HTTP API 时使用的 uKey - */ -inline class GroupImageUKey(inline val uKey: ByteArray) : GroupImageResponse { - override fun toString(): String = "GroupImageUKey(uKey=${uKey.toUHexString()})" -} - -/** - * 图片 ID 已存在 - * 发送消息时使用的 id - */ -object GroupImageAlreadyExists : GroupImageResponse { - override fun toString(): String = "GroupImageAlreadyExists" -} - -/** - * 超过文件大小上限 - */ -object GroupImageOverFileSizeMax : GroupImageResponse { - override fun toString(): String = "GroupImageOverFileSizeMax" - -} -// endregion - /** * 请求上传图片. 将发送图片的 md5, size, width, height. * 服务器返回以下之一: @@ -324,7 +152,7 @@ object FriendImagePacket : SessionPacketFactory() { imageId: ImageId ): OutgoingPacket { imageId.requireLength() - require(imageId.value.length == 37) { "ImageId.value.length must == 37" } + require(imageId.value.length == 37) { "ImageId.value.length must == 37 but given length=${imageId.value.length} value=${imageId.value}" } // 00 00 00 07 00 00 00 // [4B] @@ -500,185 +328,3 @@ object FriendImagePacket : SessionPacketFactory() { } } } - - -/** - * 获取 Image Id 和上传用的一个 uKey - */ -@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID) -@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)") -object GroupImagePacket : SessionPacketFactory() { - @Suppress("FunctionName") - fun RequestImageId( - bot: UInt, - groupInternalId: GroupInternalId, - image: ExternalImage, - sessionKey: SessionKey - ): OutgoingPacket = buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) { - writeHex("00 00 00 07 00 00") - - writeShortLVPacket(lengthOffset = { it - 7 }) { - writeByte(0x08) - writeHex("01 12 03 98 01 01 10 01 1A") - // 02 10 02 22 - - 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) - } - } - - } - - @Suppress("FunctionName") - fun RequestImageLink( - bot: UInt, - sessionKey: SessionKey, - imageId: ImageId - ): OutgoingPacket { - imageId.requireLength() - require(imageId.value.length == 37) { "ImageId.value.length must == 37" } - - // 00 00 00 07 00 00 00 - // [4B] - // 08 - // 01 12 - // 03 98 - // 01 02 - // 08 02 - // - // 1A [47] - // 08 [A2 FF 8C F0 03] UVarInt - // 10 [DD F1 92 B7 07] UVarInt - // 1A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 - // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01 - - - // 00 00 00 07 00 00 00 - // [4B] - // 08 - // 01 12 - // 03 98 - // 01 02 - // 08 02 - // - // 1A - // [47] - // 08 [A2 FF 8C F0 03] - // 10 [A6 A7 F1 EA 02] - // 1A [25] 2F 39 61 31 66 37 31 36 32 2D 38 37 30 38 2D 34 39 30 38 2D 38 31 63 30 2D 66 34 63 64 66 33 35 63 38 64 37 65 - // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01 - - - return buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) { - writeHex("00 00 00 07 00 00") - - writeUShort(0x004Bu) - - writeUByte(0x08u) - writeTV(0x01_12u) - writeTV(0x03_98u) - writeTV(0x01_02u) - writeTV(0x08_02u) - - writeUByte(0x1Au) - writeUByte(0x47u) - writeTUVarint(0x08u, bot) - writeTUVarint(0x10u, bot) - writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1)) - writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01") - } - } - - private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u) - - override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupImageResponse { - discardExact(6)//00 00 00 05 00 00 - - val length = remaining - 128 - 14 - if (length < 0) { - return if (readUShort().toUInt() == 0x0025u) GroupImageOverFileSizeMax else GroupImageAlreadyExists - } - - discardExact(length) - return GroupImageUKey(readBytes(128)) - } - - // 下载图片 - // 00 00 00 05 00 00 - // [02 46] - // 12 03 - // 98 01 02 - // 08 9B A4 D4 9A 0A 10 02 22 BB 04 - // 08 92 A8 B2 D3 0A - // 12 [10] EB 1A 34 01 8F 1E B4 73 39 34 F0 65 68 80 A7 52 - // 18 00 - // 48 BD EE 92 CD 01 - // 48 BD EE 92 E5 01 - // 48 B4 C3 A9 E8 06 - // 48 BA F6 D7 5C - // 48 EF BC A4 DC 07 - // 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 // gchat.qpic.cn - // 缩略图 62 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 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 [75] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 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 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 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 03 - // 9A 01 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 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 - - // 00 00 00 05 00 00 - // [02 46] - // 12 03 98 01 02 08 9B A4 D4 9A 0A 10 02 22 BB 04 - // 08 D1 F1 CE FD 0B - // 12 [10] 5F D4 03 6D 59 36 18 FD 49 3D 4C 97 53 02 BA D8 - // 18 00 - // 48 BD EE 92 8D 05 - // 48 BD EE 92 C5 01 - // 48 B4 C3 A9 E8 06 - // 48 BA F6 D7 5C - // 48 EF BC A4 DC 07 - // 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 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 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 [75] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 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 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 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 03 - // 9A 01 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 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/GroupImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt new file mode 100644 index 000000000..1e794428b --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt @@ -0,0 +1,224 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "RUNTIME_ANNOTATION_NOT_SUPPORTED") + +package net.mamoe.mirai.network.protocol.tim.packet.action + +import kotlinx.coroutines.withContext +import kotlinx.io.charsets.Charsets +import kotlinx.io.core.* +import kotlinx.serialization.SerialId +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoBuf +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.message.requireLength +import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.network.protocol.tim.TIMProtocol +import net.mamoe.mirai.network.protocol.tim.packet.* +import net.mamoe.mirai.network.protocol.tim.packet.event.EventPacket +import net.mamoe.mirai.qqAccount +import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.Http +import net.mamoe.mirai.utils.assertUnreachable +import net.mamoe.mirai.utils.io.* +import kotlin.coroutines.coroutineContext + + +/** + * 上传群图片 + * 挂起直到上传完成或失败 + * + * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数 + * + * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时 + */ +suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession { + val userContext = coroutineContext + val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect() + + withContext(userContext) { + when (response) { + is ImageUploadInfo -> response.uKey?.let { + Http.postImage( + htcmd = "0x6ff0071", + uin = bot.qqAccount, + groupId = GroupId(id), + imageInput = image.input, + inputSize = image.inputSize, + uKeyHex = it.toUHexString("") + ) + } + + // TODO: 2019/11/17 超过大小的情况 + //is Overfile -> throw OverFileSizeMaxException() + else -> assertUnreachable() + } + } + + return image.groupImageId +} + +interface GroupImageResponse : EventPacket + +// endregion + +@Serializable +data class ImageDownloadInfo( + @SerialId(11) val host: String, + + @SerialId(12) val thumbnail: String, + @SerialId(13) val original: String, + @SerialId(14) val compressed: String +) : GroupImageResponse + +@Serializable +class ImageUploadInfo( + @SerialId(8) val uKey: ByteArray? = null +) : GroupImageResponse { + override fun toString(): String = "ImageUploadInfo(uKey=${uKey?.toUHexString()})" +} + +/** + * 获取 Image Id 和上传用的一个 uKey + */ +@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID) +@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)") +object GroupImagePacket : SessionPacketFactory() { + @Suppress("FunctionName") + fun RequestImageId( + bot: UInt, + groupInternalId: GroupInternalId, + image: ExternalImage, + sessionKey: SessionKey + ): OutgoingPacket = buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) { + writeHex("00 00 00 07 00 00") + + writeShortLVPacket(lengthOffset = { it - 7 }) { + writeByte(0x08) + writeHex("01 12 03 98 01 01 10 01 1A") + // 02 10 02 22 + + 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) + } + } + + } + + @Suppress("FunctionName") + fun RequestImageLink( + bot: UInt, + sessionKey: SessionKey, + imageId: ImageId + ): OutgoingPacket { + imageId.requireLength() + require(imageId.value.length == 37) { "ImageId.value.length must == 37" } + + // 00 00 00 07 00 00 00 + // [4B] + // 08 + // 01 12 + // 03 98 + // 01 02 + // 08 02 + // + // 1A [47] + // 08 [A2 FF 8C F0 03] UVarInt + // 10 [DD F1 92 B7 07] UVarInt + // 1A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66 + // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01 + + + // 00 00 00 07 00 00 00 + // [4B] + // 08 01 + // 12 03 + // 98 01 02 + // 08 02 + // + // 1A + // [47] + // 08 [A2 FF 8C F0 03] + // 10 [A6 A7 F1 EA 02] + // 1A [25] 2F 39 61 31 66 37 31 36 32 2D 38 37 30 38 2D 34 39 30 38 2D 38 31 63 30 2D 66 34 63 64 66 33 35 63 38 64 37 65 + // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01 + + return buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) { + writeHex("00 00 00 07 00 00") + + writeUShort(0x004Bu) + + writeUByte(0x08u) + writeTV(0x01_12u) + writeTV(0x03_98u) + writeTV(0x01_02u) + writeTV(0x08_02u) + + writeUByte(0x1Au) + writeUByte(0x47u) + writeTUVarint(0x08u, bot) + writeTUVarint(0x10u, bot) + writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1)) + writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01") + } + } + + private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u) + + override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupImageResponse { + discardExact(6)//00 00 00 05 00 00 + + discardExact(2) // 是 protobuf 的长度, 但是是错的 + val bytes = readBytes() + // println(ByteReadPacket(bytes).readProtoMap()) + + @Serializable + data class GroupImageResponseProto( + @SerialId(3) val imageUploadInfoPacket: ImageUploadInfo? = null, + @SerialId(4) val imageDownloadInfo: ImageDownloadInfo? = null + ) + + val proto = ProtoBuf.load(GroupImageResponseProto.serializer(), bytes) + return when { + proto.imageUploadInfoPacket != null -> proto.imageUploadInfoPacket + proto.imageDownloadInfo != null -> proto.imageDownloadInfo + else -> assertUnreachable() + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt new file mode 100644 index 000000000..52f49740a --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt @@ -0,0 +1,48 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE") + +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.io.core.Input +import net.mamoe.mirai.contact.GroupId +import net.mamoe.mirai.utils.configureBody + + +@Suppress("SpellCheckingInspection") +internal suspend inline fun HttpClient.postImage( + htcmd: String, + uin: UInt, + groupId: GroupId?, + imageInput: Input, + inputSize: Long, + uKeyHex: String +): Boolean = try { + post { + 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() +} diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt new file mode 100644 index 000000000..54ba01eeb --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt @@ -0,0 +1,62 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH") + +package net.mamoe.mirai.network.protocol.tim.packet.action + +/** + * 图片文件过大 + */ +class OverFileSizeMaxException : IllegalStateException() + +/* +/** + * 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00 + */ +@Deprecated("Useless packet") +@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME) +@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)") +object SubmitImageFilenamePacket : PacketFactory { + operator fun invoke( + bot: UInt, + target: UInt, + filename: String, + sessionKey: SessionKey + ): OutgoingPacket = buildOutgoingPacket { + writeQQ(bot) + writeFully(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 + } + + @PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)") + class Response { + override fun decode() = with(input) { + require(readBytes().contentEquals(expecting)) + } + + companion object { + private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00) + } + } +}*/ +// regiion GroupImageResponse diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt new file mode 100644 index 000000000..c0167c517 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt @@ -0,0 +1,140 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE") + +package net.mamoe.mirai.utils + +import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.readBytes +import kotlinx.io.core.readUInt +import kotlinx.io.core.readULong +import net.mamoe.mirai.utils.io.UVarInt +import net.mamoe.mirai.utils.io.readUVarInt +import net.mamoe.mirai.utils.io.toUHexString + +// ProtoBuf utilities + +/* + * Type Meaning Used For + * 0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum + * 1 64-bit fixed64, sfixed64, double + * 2 Length-delimi string, bytes, embedded messages, packed repeated fields + * 3 Start group Groups (deprecated) + * 4 End group Groups (deprecated) + * 5 32-bit fixed32, sfixed32, float + * + * https://www.jianshu.com/p/f888907adaeb + */ + +@Suppress("FunctionName") +fun ProtoFieldId(serializedId: UInt): ProtoFieldId = ProtoFieldId(protoFieldNumber(serializedId), protoType(serializedId)) + +data class ProtoFieldId( + val fieldNumber: Int, + val type: ProtoType +) { + override fun toString(): String = "$type $fieldNumber" +} + +enum class ProtoType(val value: Byte, val typeName: String) { + /** + * int32, int64, uint32, uint64, sint32, sint64, bool, enum + */ + VAR_INT(0x00, "varint"), + + /** + * fixed64, sfixed64, double + */ + BIT_64(0x01, " 64bit"), + + /** + * string, bytes, embedded messages, packed repeated fields + */ + LENGTH_DELIMI(0x02, "delimi"), + + /** + * Groups (deprecated) + */ + START_GROUP(0x03, "startg"), + + /** + * Groups (deprecated) + */ + END_GROUP(0x04, " endg"), + + /** + * fixed32, sfixed32, float + */ + BIT_32(0x05, " 32bit"), ; + + override fun toString(): String = this.typeName + + companion object { + fun valueOf(value: Byte): ProtoType = values().firstOrNull { it.value == value } ?: error("Unknown ProtoId $value") + } +} + +/** + * 由 ProtoBuf 序列化后的 id 得到类型 + * + * serializedId = (fieldNumber << 3) | wireType + */ +fun protoType(number: UInt): ProtoType = ProtoType.valueOf(number.toInt().shl(29).ushr(29).toByte()) + +/** + * ProtoBuf 序列化后的 id 转为序列前标记的 id + * + * serializedId = (fieldNumber << 3) | wireType + */ +fun protoFieldNumber(number: UInt): Int = number.toInt().ushr(3) + + +class ProtoMap(map: MutableMap) : MutableMap by map { + override fun toString(): String { + return this.entries.joinToString(prefix = "ProtoMap(\n ", postfix = "\n)", separator = "\n ") { + "${it.key}=" + it.value.contentToString().replace("\n", """\n""") + } + } + + /* + override fun put(key: ProtoFieldId, value: Any): Any? { + println("${key}=" + value.contentToString()) + return null + }*/ +} + +fun Any.contentToString(): String = when (this) { + is UInt -> "0x" + this.toUHexString("") + "($this)" + is UByte -> "0x" + this.toUHexString() + "($this)" + is UShort -> "0x" + this.toUHexString("") + "($this)" + is ULong -> "0x" + this.toUHexString("") + "($this)" + is Int -> "0x" + this.toUHexString("") + "($this)" + is Byte -> "0x" + this.toUHexString() + "($this)" + is Short -> "0x" + this.toUHexString("") + "($this)" + is Long -> "0x" + this.toUHexString("") + "($this)" + + is UVarInt -> "0x" + this.toUHexString("") + "($this)" + + is Boolean -> if (this) "true" else "false" + + is ByteArray -> this.toUHexString()// + " (${this.encodeToString()})" + else -> this.toString() +} + +fun ByteReadPacket.readProtoMap(length: Long = this.remaining): ProtoMap { + val map = ProtoMap(mutableMapOf()) + + + val expectingRemaining = this.remaining - length + while (this.remaining != expectingRemaining) { + val id = ProtoFieldId(readUVarInt()) + map[id] = when (id.type) { + ProtoType.VAR_INT -> UVarInt(readUVarInt()) + ProtoType.BIT_32 -> readUInt() + ProtoType.BIT_64 -> readULong() + ProtoType.LENGTH_DELIMI -> readBytes(readUVarInt().toInt()) + + ProtoType.START_GROUP -> error("unsupported") + ProtoType.END_GROUP -> error("unsupported") + } + } + return map +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt index 9bb5c02d4..d8b64841a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt @@ -52,7 +52,7 @@ fun Long.toUHexString(separator: String = " "): String = */ fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte()) -fun UByte.toUHexString(): String = (this and 255u).toByte().toUHexString() +fun UByte.toUHexString(): String = this.toByte().toUHexString() /** * 255u -> 00 00 00 FF @@ -73,10 +73,17 @@ fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUH * 转无符号十六进制表示, 并补充首位 `0`. * 转换结果示例: `FF`, `0E` */ -fun Byte.toUHexString(): String = this.toUByte().toString(16).toUpperCase().let { - if (it.length == 1) "0$it" - else it -} +fun Byte.toUHexString(): String = this.toUByte().fixToUHex() + +/** + * 转无符号十六进制表示, 并补充首位 `0`. + */ +fun Byte.fixToUHex(): String = this.toUByte().fixToUHex() + +/** + * 转无符号十六进制表示, 并补充首位 `0`. + */ +fun UByte.fixToUHex(): String = if (this.toInt() in 0..9) "0${this.toString(16).toUpperCase()}" else this.toString(16).toUpperCase() /** * 将无符号 Hex 转为 [ByteArray], 有根据 hex 的 [hashCode] 建立的缓存. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt index 8d47ff657..f67c52a75 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt @@ -44,6 +44,9 @@ fun Input.readVarInt(): Int { return decodeZigZag32(this.readUVarInt()) } +inline class UVarInt( + val data: UInt +) @JvmSynthetic fun Input.readUVarInt(): UInt { @@ -82,6 +85,37 @@ fun Output.writeUVarLong(ulong: Long) { this.write0(ulong) } +fun UVarInt.toByteArray(): ByteArray { + val list = mutableListOf() + var value = this.data.toLong() + do { + var temp = (value and 127).toByte() + value = value ushr 7 + if (value != 0L) { + temp = temp or 128.toByte() + } + list += temp + } while (value != 0L) + return list.toByteArray() +} + +fun UVarInt.toUHexString(separator: String = " "): String = buildString { + var value = data.toLong() + + var isFirst = true + do { + if (!isFirst) { + append(separator) + } + var temp = (value and 127).toByte() + value = value ushr 7 + if (value != 0L) { + temp = temp or 128.toByte() + } + append(temp.toUByte().fixToUHex()) + isFirst = false + } while (value != 0L) +} private fun Output.write0(long: Long) { var value = long @@ -101,7 +135,7 @@ private fun read(stream: Input, maxSize: Int): Long { var b = stream.readByte().toInt() while (b and 0x80 == 0x80) { value = value or ((b and 0x7F).toLong() shl size++ * 7) - require(size < maxSize) { "VarLong too bigger(expecting maxSize=$maxSize)" } + require(size < maxSize) { "VarLong too big(expecting maxSize=$maxSize)" } b = stream.readByte().toInt() } diff --git a/mirai-debug/build.gradle.kts b/mirai-debug/build.gradle.kts index 4374bdf56..c60f062c7 100644 --- a/mirai-debug/build.gradle.kts +++ b/mirai-debug/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.openjfx.javafxplugin") version "0.0.8" kotlin("jvm") java + id("kotlinx-serialization") } javafx { @@ -15,14 +16,15 @@ application { mainClassName = "Application" } -val kotlinVersion = rootProject.ext["kotlinVersion"].toString() -val atomicFuVersion = rootProject.ext["atomicFuVersion"].toString() -val coroutinesVersion = rootProject.ext["coroutinesVersion"].toString() -val kotlinXIoVersion = rootProject.ext["kotlinXIoVersion"].toString() -val coroutinesIoVersion = rootProject.ext["coroutinesIoVersion"].toString() +val kotlinVersion: String by rootProject.ext +val atomicFuVersion: String by rootProject.ext +val coroutinesVersion: String by rootProject.ext +val kotlinXIoVersion: String by rootProject.ext +val coroutinesIoVersion: String by rootProject.ext +val serializationVersion: String by rootProject.ext -val klockVersion = rootProject.ext["klockVersion"].toString() -val ktorVersion = rootProject.ext["ktorVersion"].toString() +val klockVersion: String by rootProject.ext +val ktorVersion: String by rootProject.ext kotlin { sourceSets { @@ -32,20 +34,30 @@ kotlin { } } +fun DependencyHandlerScope.kotlinx(id: String, version: String) { + implementation("org.jetbrains.kotlinx:$id:$version") +} + +fun DependencyHandlerScope.ktor(id: String, version: String) { + implementation("io.ktor:$id:$version") +} + dependencies { - api(project(":mirai-core")) + implementation(project(":mirai-core")) runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // mpp targeting android limitation - api("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") implementation("org.pcap4j:pcap4j-distribution:1.8.2") implementation("no.tornado:tornadofx:1.7.17") compile(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2") - implementation("org.jetbrains.kotlin:kotlin-stdlib") - implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion") - implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:$kotlinXIoVersion") - implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + kotlin("kotlin-stdlib", kotlinVersion) + kotlinx("atomicfu", atomicFuVersion) + kotlinx("kotlinx-io-jvm", kotlinXIoVersion) + kotlinx("kotlinx-io", kotlinXIoVersion) + kotlinx("kotlinx-coroutines-io", coroutinesIoVersion) + kotlinx("kotlinx-coroutines-core", coroutinesVersion) + + kotlinx("kotlinx-serialization-runtime", serializationVersion) } \ No newline at end of file diff --git a/mirai-debug/src/main/kotlin/test/ProtoTest.kt b/mirai-debug/src/main/kotlin/test/ProtoTest.kt new file mode 100644 index 000000000..3f9e43fe8 --- /dev/null +++ b/mirai-debug/src/main/kotlin/test/ProtoTest.kt @@ -0,0 +1,57 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS") + +package test + +import kotlinx.serialization.ImplicitReflectionSerializer +import kotlinx.serialization.SerialId +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoBuf +import kotlinx.serialization.protobuf.ProtoNumberType +import kotlinx.serialization.protobuf.ProtoType +import kotlinx.serialization.serializer +import net.mamoe.mirai.network.protocol.tim.packet.action.ImageUploadInfo +import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.ProtoFieldId +import net.mamoe.mirai.utils.io.hexToBytes +import net.mamoe.mirai.utils.io.toUHexString +import kotlin.reflect.KClass + +@Serializable +data class ProtoTest( + @SerialId(1) val string: String, + @SerialId(1) val int: Int, + @SerialId(1) val boolean: Boolean, + @SerialId(1) val short: Short, + @SerialId(1) val byte: Byte, + @SerialId(1) @ProtoType(ProtoNumberType.FIXED) val fixedByte: Byte +) + +@UseExperimental(MiraiInternalAPI::class) +fun main() { + deserializeTest() + return + + println(ProtoFieldId(0x12u)) + + intArrayOf( + 0x5A, + 0X62, + 0X6A, + 0X72 + ).forEach { + println(it.toUShort().toUHexString() + " => " + ProtoFieldId(it.toUInt())) + } + + println(ProtoBuf.dump(ProtoTest.serializer(), ProtoTest("ss", 1, false, 1, 1, 1)).toUHexString()) +} + +fun deserializeTest() { + val bytes = + ("08 00 10 00 20 01 2A 1E 0A 10 BF 83 4C 2B 67 47 41 8C 9F DD 6D 8C 8E 95 53 D6 10 04 18 E4 E0 54 20 B0 09 28 9E 0D 30 FB AE A6 F4 07 38 50 48 D8 92 9E CD 0A") + .hexToBytes() + + println(ImageUploadInfo::class.loadFrom(bytes)) +} + +@UseExperimental(ImplicitReflectionSerializer::class) +fun KClass.loadFrom(protoBuf: ByteArray): T = ProtoBuf.load(this.serializer(), protoBuf) \ No newline at end of file diff --git a/mirai-demos/mirai-demo-gentleman/build.gradle b/mirai-demos/mirai-demo-gentleman/build.gradle index de7c93b86..e85e6b802 100644 --- a/mirai-demos/mirai-demo-gentleman/build.gradle +++ b/mirai-demos/mirai-demo-gentleman/build.gradle @@ -6,12 +6,12 @@ dependencies { api project(":mirai-core") runtime files("../../mirai-core/build/classes/kotlin/jvm/main") // mpp targeting android limitation //runtime files("../../mirai-core/build/classes/atomicfu/jvm/main") // mpp targeting android limitation - api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion - api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion + implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion + implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion") - compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62' + implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.62' implementation 'org.jsoup:jsoup:1.12.1' } diff --git a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt index 2499937ea..e25d13f94 100644 --- a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt +++ b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.mamoe.mirai.* +import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.event.Subscribable import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeMessages @@ -15,6 +16,7 @@ import net.mamoe.mirai.message.Image import net.mamoe.mirai.message.getValue import net.mamoe.mirai.message.sendAsImageTo import net.mamoe.mirai.network.protocol.tim.packet.event.FriendMessage +import net.mamoe.mirai.network.protocol.tim.packet.event.GroupMessage import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess import java.io.File import java.util.* @@ -65,7 +67,7 @@ suspend fun main() { } has { - if (this is FriendMessage) { + if (this is FriendMessage || (this is GroupMessage && this.permission == MemberPermission.OPERATOR)) { withContext(IO) { val image: Image by message // 等同于 val image = message[Image]