mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-20 15:24:45 +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
|
||||
|
||||
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
|
||||
}
|
@ -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.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] 建立的缓存.
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
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")
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user