ProtoBuf implementation

This commit is contained in:
Him188 2019-11-17 16:36:29 +08:00
parent d4bb6f1581
commit d87c7f629a
11 changed files with 614 additions and 382 deletions

View File

@ -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<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()
}
@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
@ -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<FriendImageResponse>() {
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<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
}

View File

@ -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()
}
}
}

View File

@ -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()
}

View File

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

View 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
}

View File

@ -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] 建立的缓存.

View File

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

View File

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

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

View File

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

View File

@ -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<Image> {
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]