mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-20 18:19:14 +08:00
ProtoBuf implementation
This commit is contained in:
parent
d4bb6f1581
commit
d87c7f629a
@ -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
|
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.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.charsets.Charsets
|
||||||
import kotlinx.io.core.*
|
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.ImageId
|
||||||
import net.mamoe.mirai.message.requireLength
|
import net.mamoe.mirai.message.requireLength
|
||||||
import net.mamoe.mirai.network.BotNetworkHandler
|
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.qqAccount
|
||||||
import net.mamoe.mirai.utils.ExternalImage
|
import net.mamoe.mirai.utils.ExternalImage
|
||||||
import net.mamoe.mirai.utils.Http
|
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.utils.io.*
|
||||||
import net.mamoe.mirai.withSession
|
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<GroupImageResponse>()
|
|
||||||
|
|
||||||
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()
|
}.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")
|
|
||||||
@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
|
// region FriendImageResponse
|
||||||
|
|
||||||
@ -224,44 +90,6 @@ object FriendImageOverFileSizeMax : FriendImageResponse {
|
|||||||
|
|
||||||
// endregion
|
// 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.
|
* 请求上传图片. 将发送图片的 md5, size, width, height.
|
||||||
* 服务器返回以下之一:
|
* 服务器返回以下之一:
|
||||||
@ -324,7 +152,7 @@ object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
|
|||||||
imageId: ImageId
|
imageId: ImageId
|
||||||
): OutgoingPacket {
|
): OutgoingPacket {
|
||||||
imageId.requireLength()
|
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
|
// 00 00 00 07 00 00 00
|
||||||
// [4B]
|
// [4B]
|
||||||
@ -500,185 +328,3 @@ object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Image Id 和上传用的一个 uKey
|
|
||||||
*/
|
|
||||||
@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID)
|
|
||||||
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
|
|
||||||
object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
|
|
||||||
@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
|
|
||||||
}
|
|
@ -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<GroupImageResponse>()
|
||||||
|
|
||||||
|
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<GroupImageResponse>() {
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<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()
|
||||||
|
}
|
@ -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
|
140
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt
Normal file
140
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt
Normal file
@ -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<ProtoFieldId, Any>) : MutableMap<ProtoFieldId, Any> 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
|
||||||
|
}
|
@ -52,7 +52,7 @@ fun Long.toUHexString(separator: String = " "): String =
|
|||||||
*/
|
*/
|
||||||
fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte())
|
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
|
* 255u -> 00 00 00 FF
|
||||||
@ -73,10 +73,17 @@ fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUH
|
|||||||
* 转无符号十六进制表示, 并补充首位 `0`.
|
* 转无符号十六进制表示, 并补充首位 `0`.
|
||||||
* 转换结果示例: `FF`, `0E`
|
* 转换结果示例: `FF`, `0E`
|
||||||
*/
|
*/
|
||||||
fun Byte.toUHexString(): String = this.toUByte().toString(16).toUpperCase().let {
|
fun Byte.toUHexString(): String = this.toUByte().fixToUHex()
|
||||||
if (it.length == 1) "0$it"
|
|
||||||
else it
|
/**
|
||||||
}
|
* 转无符号十六进制表示, 并补充首位 `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] 建立的缓存.
|
* 将无符号 Hex 转为 [ByteArray], 有根据 hex 的 [hashCode] 建立的缓存.
|
||||||
|
@ -44,6 +44,9 @@ fun Input.readVarInt(): Int {
|
|||||||
return decodeZigZag32(this.readUVarInt())
|
return decodeZigZag32(this.readUVarInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline class UVarInt(
|
||||||
|
val data: UInt
|
||||||
|
)
|
||||||
|
|
||||||
@JvmSynthetic
|
@JvmSynthetic
|
||||||
fun Input.readUVarInt(): UInt {
|
fun Input.readUVarInt(): UInt {
|
||||||
@ -82,6 +85,37 @@ fun Output.writeUVarLong(ulong: Long) {
|
|||||||
this.write0(ulong)
|
this.write0(ulong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun UVarInt.toByteArray(): ByteArray {
|
||||||
|
val list = mutableListOf<Byte>()
|
||||||
|
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) {
|
private fun Output.write0(long: Long) {
|
||||||
var value = long
|
var value = long
|
||||||
@ -101,7 +135,7 @@ private fun read(stream: Input, maxSize: Int): Long {
|
|||||||
var b = stream.readByte().toInt()
|
var b = stream.readByte().toInt()
|
||||||
while (b and 0x80 == 0x80) {
|
while (b and 0x80 == 0x80) {
|
||||||
value = value or ((b and 0x7F).toLong() shl size++ * 7)
|
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()
|
b = stream.readByte().toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ plugins {
|
|||||||
id("org.openjfx.javafxplugin") version "0.0.8"
|
id("org.openjfx.javafxplugin") version "0.0.8"
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
java
|
java
|
||||||
|
id("kotlinx-serialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
@ -15,14 +16,15 @@ application {
|
|||||||
mainClassName = "Application"
|
mainClassName = "Application"
|
||||||
}
|
}
|
||||||
|
|
||||||
val kotlinVersion = rootProject.ext["kotlinVersion"].toString()
|
val kotlinVersion: String by rootProject.ext
|
||||||
val atomicFuVersion = rootProject.ext["atomicFuVersion"].toString()
|
val atomicFuVersion: String by rootProject.ext
|
||||||
val coroutinesVersion = rootProject.ext["coroutinesVersion"].toString()
|
val coroutinesVersion: String by rootProject.ext
|
||||||
val kotlinXIoVersion = rootProject.ext["kotlinXIoVersion"].toString()
|
val kotlinXIoVersion: String by rootProject.ext
|
||||||
val coroutinesIoVersion = rootProject.ext["coroutinesIoVersion"].toString()
|
val coroutinesIoVersion: String by rootProject.ext
|
||||||
|
val serializationVersion: String by rootProject.ext
|
||||||
|
|
||||||
val klockVersion = rootProject.ext["klockVersion"].toString()
|
val klockVersion: String by rootProject.ext
|
||||||
val ktorVersion = rootProject.ext["ktorVersion"].toString()
|
val ktorVersion: String by rootProject.ext
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
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 {
|
dependencies {
|
||||||
api(project(":mirai-core"))
|
implementation(project(":mirai-core"))
|
||||||
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // mpp targeting android limitation
|
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("org.pcap4j:pcap4j-distribution:1.8.2")
|
||||||
implementation("no.tornado:tornadofx:1.7.17")
|
implementation("no.tornado:tornadofx:1.7.17")
|
||||||
compile(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2")
|
compile(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
kotlin("kotlin-stdlib", kotlinVersion)
|
||||||
implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
|
kotlinx("atomicfu", atomicFuVersion)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:$kotlinXIoVersion")
|
kotlinx("kotlinx-io-jvm", kotlinXIoVersion)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion")
|
kotlinx("kotlinx-io", kotlinXIoVersion)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion")
|
kotlinx("kotlinx-coroutines-io", coroutinesIoVersion)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
kotlinx("kotlinx-coroutines-core", coroutinesVersion)
|
||||||
|
|
||||||
|
kotlinx("kotlinx-serialization-runtime", serializationVersion)
|
||||||
}
|
}
|
57
mirai-debug/src/main/kotlin/test/ProtoTest.kt
Normal file
57
mirai-debug/src/main/kotlin/test/ProtoTest.kt
Normal file
@ -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 <T : Any> KClass<T>.loadFrom(protoBuf: ByteArray): T = ProtoBuf.load(this.serializer(), protoBuf)
|
@ -6,12 +6,12 @@ dependencies {
|
|||||||
api project(":mirai-core")
|
api project(":mirai-core")
|
||||||
runtime files("../../mirai-core/build/classes/kotlin/jvm/main") // mpp targeting android limitation
|
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
|
//runtime files("../../mirai-core/build/classes/atomicfu/jvm/main") // mpp targeting android limitation
|
||||||
api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
|
implementation 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.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion")
|
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'
|
implementation 'org.jsoup:jsoup:1.12.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.mamoe.mirai.*
|
import net.mamoe.mirai.*
|
||||||
|
import net.mamoe.mirai.contact.MemberPermission
|
||||||
import net.mamoe.mirai.event.Subscribable
|
import net.mamoe.mirai.event.Subscribable
|
||||||
import net.mamoe.mirai.event.subscribeAlways
|
import net.mamoe.mirai.event.subscribeAlways
|
||||||
import net.mamoe.mirai.event.subscribeMessages
|
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.getValue
|
||||||
import net.mamoe.mirai.message.sendAsImageTo
|
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.FriendMessage
|
||||||
|
import net.mamoe.mirai.network.protocol.tim.packet.event.GroupMessage
|
||||||
import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
|
import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -65,7 +67,7 @@ suspend fun main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
has<Image> {
|
has<Image> {
|
||||||
if (this is FriendMessage) {
|
if (this is FriendMessage || (this is GroupMessage && this.permission == MemberPermission.OPERATOR)) {
|
||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
val image: Image by message
|
val image: Image by message
|
||||||
// 等同于 val image = message[Image]
|
// 等同于 val image = message[Image]
|
||||||
|
Loading…
Reference in New Issue
Block a user