Commit 35dca403 authored by Him188's avatar Him188

Image upload

parent 8e54e716
package net.mamoe.mirai.qqandroid
import kotlinx.io.core.readBytes
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.NotOnlineImageFromFile
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.network.highway.Highway
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.CSDataHighwayHead
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.withUse
import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.cryptor.contentToString
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.io.PlatformSocket
import net.mamoe.mirai.utils.io.toUHexString
import net.mamoe.mirai.utils.unsafeWeakRef
import kotlin.coroutines.CoroutineContext
......@@ -116,7 +127,73 @@ internal class GroupImpl(
}
override suspend fun uploadImage(image: ExternalImage): Image {
TODO("not implemented")
bot.network.run {
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
bot.client,
uin = bot.uin,
groupCode = id,
md5 = image.md5,
size = image.inputSize,
picWidth = image.width,
picHeight = image.height,
picType = image.imageType,
filename = image.filename
).sendAndExpect()
when (response) {
is ImgStore.GroupPicUp.Response.Failed -> error("upload group image failed with reason ${response.message}")
is ImgStore.GroupPicUp.Response.FileExists -> {
val resourceId = image.calculateImageResourceId()
return NotOnlineImageFromFile(
resourceId = resourceId,
md5 = response.fileInfo.fileMd5,
filepath = resourceId,
fileLength = response.fileInfo.fileSize.toInt(),
height = response.fileInfo.fileHeight,
width = response.fileInfo.fileWidth,
imageType = response.fileInfo.fileType
)
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
val socket = PlatformSocket()
socket.connect(response.uploadIpList.first().toIpV4AddressString().also { println("serverIp=$it") }, response.uploadPortList.first())
// socket.use {
socket.send(
Highway.RequestDataTrans(
uin = bot.uin,
command = "PicUp.DataUp",
buildVer = bot.client.buildVer,
uKey = response.uKey,
data = image.input,
dataSize = image.inputSize.toInt(),
md5 = image.md5,
sequenceId = bot.client.nextHighwayDataTransSequenceId()
)
)
// }
//0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
socket.read().withUse {
readByte()
val headLength = readInt()
val bodyLength = readInt()
val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
println(proto.contentToString())
println(readBytes(bodyLength).toUHexString())
}
val resourceId = image.calculateImageResourceId()
return NotOnlineImageFromFile(
resourceId = resourceId,
md5 = image.md5,
filepath = resourceId,
fileLength = image.inputSize.toInt(),
height = image.height,
width = image.width,
imageType = image.imageType
)
}
}
}
}
}
\ No newline at end of file
......@@ -95,7 +95,8 @@ internal open class QQAndroidClient(
var openAppId: Long = 715019303L
val apkVersionName: ByteArray = "8.2.0".toByteArray()
val apkVersionName: ByteArray get() = "8.2.0".toByteArray()
val buildVer: String get() = "8.2.0.1296"
private val messageSequenceId: AtomicInt = atomic(0)
internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2)
......@@ -103,6 +104,9 @@ internal open class QQAndroidClient(
private val requestPacketRequestId: AtomicInt = atomic(1921334513)
internal fun nextRequestPacketRequestId(): Int = requestPacketRequestId.getAndAdd(2)
private val highwayDataTransSequenceId: AtomicInt = atomic(87017)
internal fun nextHighwayDataTransSequenceId(): Int = highwayDataTransSequenceId.getAndAdd(2)
val appClientVersion: Int = 0
var networkType: NetworkType = NetworkType.WIFI
......
package net.mamoe.mirai.qqandroid.network.highway
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol
import io.ktor.http.content.OutgoingContent
import io.ktor.http.userAgent
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.CSDataHighwayHead
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.utils.io.ByteArrayPool
@Suppress("SpellCheckingInspection")
internal suspend inline fun HttpClient.postImage(
htcmd: String,
uin: Long,
groupcode: Long?,
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.toString()
if (groupcode != null) parameters["groupcode"] = groupcode.toString()
parameters["term"] = "pc"
parameters["ver"] = "5603"
parameters["filesize"] = inputSize.toString()
parameters["range"] = 0.toString()
parameters["ukey"] = uKeyHex
userAgent("QQClient")
}
body = object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType = ContentType.Image.Any
override val contentLength: Long = inputSize
override suspend fun writeTo(channel: ByteWriteChannel) {
ByteArrayPool.useInstance { buffer: ByteArray ->
var size: Int
while (imageInput.readAvailable(buffer).also { size = it } != 0) {
channel.writeFully(buffer, 0, size)
}
}
}
}
} == HttpStatusCode.OK
} finally {
imageInput.close()
}
object Highway {
fun RequestDataTrans(
uin: Long,
command: String,
sequenceId: Int,
buildVer: String,
appId: Int = 537062845,
dataFlag: Int = 4096,
commandId: Int = 2,
localId: Int = 2052,
uKey: ByteArray,
data: Input,
dataSize: Int,
md5: ByteArray
): ByteReadPacket {
val dataHighwayHead = CSDataHighwayHead.DataHighwayHead(
version = 1,
uin = uin.toString(),
command = command,
seq = sequenceId,
retryTimes = 0,
appid = appId,
dataflag = dataFlag,
commandId = commandId,
buildVer = buildVer,
localeId = localId
)
val segHead = CSDataHighwayHead.SegHead(
datalength = dataSize,
filesize = dataSize.toLong() and 0xFFffFFff,
serviceticket = uKey,
md5 = md5,
fileMd5 = md5
)
return Codec.buildC2SData(dataHighwayHead, segHead, EMPTY_BYTE_ARRAY, null, data, dataSize)
}
private object Codec {
fun buildC2SData(
dataHighwayHead: CSDataHighwayHead.DataHighwayHead,
segHead: CSDataHighwayHead.SegHead,
extendInfo: ByteArray,
loginSigHead: CSDataHighwayHead.LoginSigHead?,
body: Input,
bodySize: Int
): ByteReadPacket {
val head = CSDataHighwayHead.ReqDataHighwayHead(
msgBasehead = dataHighwayHead,
msgSeghead = segHead,
reqExtendinfo = extendInfo,
msgLoginSigHead = loginSigHead
).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
return buildPacket {
writeByte(40)
writeInt(head.size)
writeInt(bodySize)
writeFully(head)
body.copyTo(this)
writeByte(41)
}
}
}
}
\ No newline at end of file
......@@ -5,6 +5,9 @@ import kotlinx.io.pool.useInstance
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Subscribable
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImageUpPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
......@@ -117,7 +120,10 @@ internal object KnownPacketFactories {
MessageSvc.PbSendMsg,
FriendList.GetFriendGroupList,
FriendList.GetTroopListSimplify,
FriendList.GetTroopMemberList
FriendList.GetTroopMemberList,
ImgStore.GroupPicUp,
ImageUpPacket,
LongConn.OffPicDown
)
object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(
......
......@@ -10,15 +10,13 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352Packet
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.UploadImgReq
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
internal object ImageUpPacket : OutgoingPacketFactory<ImageUpPacket.ImageUpPacketResponse>("LongConn.OffPicUp") {
operator fun invoke(client: QQAndroidClient, req: UploadImgReq): OutgoingPacket {
// TODO: 2020/1/24 测试: bodyType, subAppId
return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
writeSsoPacket(client, subAppId = 0, commandName = commandName, sequenceId = it) {
return buildOutgoingUniPacket(client) {
val data = ProtoBufWithNullableSupport.dump(
Cmd0x352Packet.serializer(),
Cmd0x352Packet.createByImageRequest(req)
......@@ -27,7 +25,6 @@ internal object ImageUpPacket : OutgoingPacketFactory<ImageUpPacket.ImageUpPacke
writeFully(data)
}
}
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): ImageUpPacketResponse {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
......
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
import io.ktor.client.HttpClient
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.writeProtoBuf
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x388
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
internal class ImgStore {
object GroupPicUp : OutgoingPacketFactory<GroupPicUp.Response>("ImgStore.GroupPicUp") {
operator fun invoke(
client: QQAndroidClient,
uin: Long,
groupCode: Long,
md5: ByteArray,
size: Long,
picWidth: Int,
picHeight: Int,
picType: Int = 1000,
fileId: Long = 0,
filename: String,
srcTerm: Int = 5,
platformType: Int = 9,
buType: Int = 1,
appPicType: Int = 1006,
originalPic: Int = 0
): OutgoingPacket = buildOutgoingUniPacket(client) {
writeProtoBuf(
Cmd0x388.ReqBody.serializer(),
Cmd0x388.ReqBody(
netType = 3, // wifi
subcmd = 1,
msgTryupImgReq = listOf(
Cmd0x388.TryUpImgReq(
groupCode = groupCode,
srcUin = uin,
fileMd5 = md5,
fileSize = size,
fileId = fileId,
fileName = filename,
picWidth = picWidth,
picHeight = picHeight,
picType = picType,
appPicType = appPicType,
buildVer = client.buildVer,
srcTerm = srcTerm,
platformType = platformType,
originalPic = originalPic,
buType = buType
)
)
)
)
}
sealed class Response : Packet {
class FileExists(
val fileId: Long,
val fileInfo: Cmd0x388.ImgInfo
) : Response() {
override fun toString(): String {
return "FileExists(fileId=$fileId, fileInfo=$fileInfo)"
}
}
class RequireUpload(
val fileId: Long,
val uKey: ByteArray,
val uploadIpList: List<Int>,
val uploadPortList: List<Int>
) : Response() {
override fun toString(): String {
return "RequireUpload(fileId=$fileId, uKey=${uKey.contentToString()})"
}
}
data class Failed(
val resultCode: Int,
val message: String
) : Response()
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp0 = readProtoBuf(Cmd0x388.RspBody.serializer())
resp0.msgTryupImgRsp ?: error("cannot find `msgTryupImgRsp` from `Cmd0x388.RspBody`")
val resp = resp0.msgTryupImgRsp.first()
return when {
resp.result != 0 -> Response.Failed(resultCode = resp.result, message = resp.failMsg)
resp.boolFileExit -> Response.FileExists(fileId = resp.fileid, fileInfo = resp.msgImgInfo!!)
else -> Response.RequireUpload(fileId = resp.fileid, uKey = resp.upUkey, uploadIpList = resp.uint32UpIp!!, uploadPortList = resp.uint32UpPort!!)
}
}
}
}
\ No newline at end of file
......@@ -13,8 +13,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
internal object ImageDownPacket : OutgoingPacketFactory<ImageDownPacket.ImageDownPacketResponse>("LongConn.OffPicDown") {
internal class LongConn {
object OffPicDown : OutgoingPacketFactory<OffPicDown.ImageDownPacketResponse>("LongConn.OffPicDown"){
operator fun invoke(client: QQAndroidClient, req: GetImgUrlReq): OutgoingPacket {
// TODO: 2020/1/24 测试: bodyType, subAppId
return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
......@@ -37,6 +38,5 @@ internal object ImageDownPacket : OutgoingPacketFactory<ImageDownPacket.ImageDow
sealed class ImageDownPacketResponse : Packet {
object Success : ImageDownPacketResponse()
}
}
}
\ No newline at end of file
......@@ -6,7 +6,9 @@ import java.io.File
fun main() {
println(
File("""/Users/jiahua.liu/Desktop/QQAndroid-F/app/src/main/java/tencent/im/s2c/msgtype0x210/submsgtype0xc7/bussinfo/mutualmark""")
File("""
E:\Projects\QQAndroidFF\app\src\main\java\com\tencent\mobileqq\highway\protocol
""".trimIndent())
.generateUnarrangedClasses().toMutableList().arrangeClasses().joinToString("\n\n")
)
}
......
......@@ -31,8 +31,6 @@ interface Group : Contact, CoroutineScope {
/**
* 在 [Group] 实例创建的时候查询一次. 并与事件同步事件更新
*
* **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新
*/
val members: ContactList<Member>
......
......@@ -7,18 +7,10 @@ import kotlinx.io.core.Input
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.io.toUHexString
@Suppress("FunctionName")
fun ExternalImage(
width: Int,
height: Int,
md5: ByteArray,
format: String,
data: ByteReadPacket
): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining)
/**
* 外部图片. 图片数据还没有读取到内存.
*
......@@ -33,19 +25,53 @@ class ExternalImage(
val md5: ByteArray,
imageFormat: String,
val input: Input,
val inputSize: Long
val inputSize: Long,
val filename: String
) {
private val format: String
companion object {
operator fun invoke(
width: Int,
height: Int,
md5: ByteArray,
format: String,
data: ByteReadPacket,
filename: String
): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining, filename)
}
init {
if (imageFormat == "JPEG" || imageFormat == "jpeg") {//必须转换
this.format = "jpg"
} else {
this.format = imageFormat
private val format: String = when (val it =imageFormat.toLowerCase()) {
"jpeg" -> "jpg" //必须转换
else -> it
}
/**
*
* ImgType:
* JPG: 1000
* PNG: 1001
* WEBP: 1002
* BMP: 1005
* GIG: 2000
* APNG: 2001
* SHARPP: 1004
*/
val imageType: Int
get() = when (format){
"jpg" -> 1000
"png" -> 1001
"webp" -> 1002
"bmp" -> 1005
"gig" -> 2000
"apng" -> 2001
"sharpp" -> 1004
else -> 1000 // unsupported, just make it jpg
}
override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
fun calculateImageResourceId(): String {
return "{${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}}.$format"
}
}
/**
......
......@@ -12,6 +12,7 @@ import kotlinx.io.core.copyTo
import kotlinx.io.errors.IOException
import kotlinx.io.streams.asInput
import kotlinx.io.streams.asOutput
import net.mamoe.mirai.utils.io.getRandomString
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
......@@ -44,7 +45,7 @@ fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
})
}
return ExternalImage(width, height, digest.digest(), formatName, buffer)
return ExternalImage(width, height, digest.digest(), formatName, buffer, getRandomString(10) + "." + formatName)
}
suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
......@@ -66,7 +67,8 @@ fun File.toExternalImage(): ExternalImage {
md5 = this.inputStream().use { it.md5() },
imageFormat = image.formatName,
input = this.inputStream().asInput(IoBuffer.Pool),
inputSize = this.length()
inputSize = this.length(),
filename = this.name
)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment