Powerful ExternalImage: support various types of input

This commit is contained in:
Him188 2020-02-23 12:22:51 +08:00
parent 6e2c8079ac
commit 5590ef510d
4 changed files with 122 additions and 49 deletions

View File

@ -10,6 +10,7 @@
package net.mamoe.mirai.qqandroid package net.mamoe.mirai.qqandroid
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.io.core.Closeable
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.* import net.mamoe.mirai.data.*
import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.broadcast
@ -40,6 +41,7 @@ internal abstract class ContactImpl : Contact {
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@Suppress("DuplicatedCode")
if (this === other) return true if (this === other) return true
if (other !is Contact) return false if (other !is Contact) return false
if (this::class != other::class) return false if (this::class != other::class) return false
@ -144,7 +146,7 @@ internal class QQImpl(
} }
} }
} finally { } finally {
image.input.close() (image.input as? Closeable)?.close()
} }
@MiraiExperimentalAPI @MiraiExperimentalAPI
@ -642,7 +644,7 @@ internal class GroupImpl(
} }
} }
} finally { } finally {
image.input.close() (image.input as Closeable)?.close()
} }
override fun toString(): String { override fun toString(): String {

View File

@ -9,13 +9,17 @@
package net.mamoe.mirai.qqandroid.network.highway package net.mamoe.mirai.qqandroid.network.highway
import io.ktor.utils.io.ByteReadChannel
import kotlinx.io.InputStream
import kotlinx.io.core.* import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray 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.data.proto.CSDataHighwayHead
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.utils.io.ByteArrayPool
object Highway { object Highway {
fun RequestDataTrans( suspend fun RequestDataTrans(
uin: Long, uin: Long,
command: String, command: String,
sequenceId: Int, sequenceId: Int,
@ -25,10 +29,11 @@ object Highway {
localId: Int = 2052, localId: Int = 2052,
uKey: ByteArray, uKey: ByteArray,
data: Input, data: Any,
dataSize: Int, dataSize: Int,
md5: ByteArray md5: ByteArray
): ByteReadPacket { ): ByteReadPacket {
require(data is Input || data is InputStream || data is ByteReadChannel) { "unsupported data: ${data::class.simpleName}" }
require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" } require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" }
require(data !is ByteReadPacket || data.remaining.toInt() == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as ByteReadPacket).remaining}" } require(data !is ByteReadPacket || data.remaining.toInt() == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as ByteReadPacket).remaining}" }
require(data !is IoBuffer || data.readRemaining == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as IoBuffer).readRemaining}" } require(data !is IoBuffer || data.readRemaining == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as IoBuffer).readRemaining}" }
@ -58,14 +63,15 @@ object Highway {
} }
private object Codec { private object Codec {
fun buildC2SData( suspend fun buildC2SData(
dataHighwayHead: CSDataHighwayHead.DataHighwayHead, dataHighwayHead: CSDataHighwayHead.DataHighwayHead,
segHead: CSDataHighwayHead.SegHead, segHead: CSDataHighwayHead.SegHead,
extendInfo: ByteArray, extendInfo: ByteArray,
loginSigHead: CSDataHighwayHead.LoginSigHead?, loginSigHead: CSDataHighwayHead.LoginSigHead?,
body: Input, body: Any,
bodySize: Int bodySize: Int
): ByteReadPacket { ): ByteReadPacket {
require(body is Input || body is InputStream || body is ByteReadChannel) { "unsupported body: ${body::class.simpleName}" }
val head = CSDataHighwayHead.ReqDataHighwayHead( val head = CSDataHighwayHead.ReqDataHighwayHead(
msgBasehead = dataHighwayHead, msgBasehead = dataHighwayHead,
msgSeghead = segHead, msgSeghead = segHead,
@ -78,7 +84,27 @@ object Highway {
writeInt(head.size) writeInt(head.size)
writeInt(bodySize) writeInt(bodySize)
writeFully(head) writeFully(head)
check(body.copyTo(this).toInt() == bodySize) { "bad body size" } when (body) {
is ByteReadPacket -> writePacket(body)
is Input -> ByteArrayPool.useInstance { buffer ->
var size: Int
while (body.readAvailable(buffer).also { size = it } != 0) {
this@buildPacket.writeFully(buffer, 0, size)
}
}
is ByteReadChannel -> ByteArrayPool.useInstance { buffer ->
var size: Int
while (body.readAvailable(buffer, 0, buffer.size).also { size = it } != 0) {
this@buildPacket.writeFully(buffer, 0, size)
}
}
is InputStream -> ByteArrayPool.useInstance { buffer ->
var size: Int
while (body.read(buffer).also { size = it } != 0) {
this@buildPacket.writeFully(buffer, 0, size)
}
}
}
writeByte(41) writeByte(41)
} }
} }

View File

@ -16,6 +16,9 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol import io.ktor.http.URLProtocol
import io.ktor.http.content.OutgoingContent import io.ktor.http.content.OutgoingContent
import io.ktor.http.userAgent import io.ktor.http.userAgent
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import kotlinx.io.InputStream
import kotlinx.io.core.Input import kotlinx.io.core.Input
import kotlinx.io.core.readAvailable import kotlinx.io.core.readAvailable
import kotlinx.io.core.use import kotlinx.io.core.use
@ -35,47 +38,55 @@ internal suspend inline fun HttpClient.postImage(
htcmd: String, htcmd: String,
uin: Long, uin: Long,
groupcode: Long?, groupcode: Long?,
imageInput: Input, imageInput: Any, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor
inputSize: Long, inputSize: Long,
uKeyHex: String uKeyHex: String
): Boolean = try { ): Boolean = post<HttpStatusCode> {
post<HttpStatusCode> { url {
url { protocol = URLProtocol.HTTP
protocol = URLProtocol.HTTP host = "htdata2.qq.com"
host = "htdata2.qq.com" path("cgi-bin/httpconn")
path("cgi-bin/httpconn")
parameters["htcmd"] = htcmd parameters["htcmd"] = htcmd
parameters["uin"] = uin.toString() parameters["uin"] = uin.toString()
if (groupcode != null) parameters["groupcode"] = groupcode.toString() if (groupcode != null) parameters["groupcode"] = groupcode.toString()
parameters["term"] = "pc" parameters["term"] = "pc"
parameters["ver"] = "5603" parameters["ver"] = "5603"
parameters["filesize"] = inputSize.toString() parameters["filesize"] = inputSize.toString()
parameters["range"] = 0.toString() parameters["range"] = 0.toString()
parameters["ukey"] = uKeyHex parameters["ukey"] = uKeyHex
userAgent("QQClient") userAgent("QQClient")
} }
body = object : OutgoingContent.WriteChannelContent() { body = object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType = ContentType.Image.Any override val contentType: ContentType = ContentType.Image.Any
override val contentLength: Long = inputSize override val contentLength: Long = inputSize
override suspend fun writeTo(channel: io.ktor.utils.io.ByteWriteChannel) { override suspend fun writeTo(channel: io.ktor.utils.io.ByteWriteChannel) {
ByteArrayPool.useInstance { buffer: ByteArray -> ByteArrayPool.useInstance { buffer: ByteArray ->
var size: Int when (imageInput) {
while (imageInput.readAvailable(buffer).also { size = it } != 0) { is Input -> {
channel.writeFully(buffer, 0, size) var size: Int
while (imageInput.readAvailable(buffer).also { size = it } != 0) {
channel.writeFully(buffer, 0, size)
}
} }
is ByteReadChannel -> imageInput.copyAndClose(channel)
is InputStream -> {
var size: Int
while (imageInput.read(buffer).also { size = it } != 0) {
channel.writeFully(buffer, 0, size)
}
}
else -> error("unsupported imageInput: ${imageInput::class.simpleName}")
} }
} }
} }
} == HttpStatusCode.OK }
} finally { } == HttpStatusCode.OK
imageInput.close()
}
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
internal object HighwayHelper { internal object HighwayHelper {
@ -84,11 +95,12 @@ internal object HighwayHelper {
serverIp: String, serverIp: String,
serverPort: Int, serverPort: Int,
uKey: ByteArray, uKey: ByteArray,
imageInput: Input, imageInput: Any,
inputSize: Int, inputSize: Int,
md5: ByteArray, md5: ByteArray,
commandId: Int // group=2, friend=1 commandId: Int // group=2, friend=1
) { ) {
require(imageInput is Input || imageInput is InputStream || imageInput is ByteReadChannel) { "unsupported imageInput: ${imageInput::class.simpleName}" }
require(md5.size == 16) { "bad md5. Required size=16, got ${md5.size}" } require(md5.size == 16) { "bad md5. Required size=16, got ${md5.size}" }
require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" } require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" }
require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" } require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" }
@ -96,6 +108,8 @@ internal object HighwayHelper {
val socket = PlatformSocket() val socket = PlatformSocket()
socket.connect(serverIp, serverPort) socket.connect(serverIp, serverPort)
socket.use { socket.use {
// TODO: 2020/2/23 使用缓存, 或使用 HTTP 发送更好 (因为无需读取到内存)
socket.send( socket.send(
Highway.RequestDataTrans( Highway.RequestDataTrans(
uin = client.uin, uin = client.uin,

View File

@ -11,6 +11,8 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import io.ktor.utils.io.ByteReadChannel
import kotlinx.io.InputStream
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Input import kotlinx.io.core.Input
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
@ -29,29 +31,58 @@ import net.mamoe.mirai.utils.io.toUHexString
* @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人 * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人
* @See ExternalImage.upload 上传图片并得到 [Image] 消息 * @See ExternalImage.upload 上传图片并得到 [Image] 消息
*/ */
class ExternalImage( class ExternalImage private constructor(
val width: Int, val width: Int,
val height: Int, val height: Int,
val md5: ByteArray, val md5: ByteArray,
imageFormat: String, imageFormat: String,
val input: Input, val input: Any, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor
val inputSize: Long, // dont be greater than Int.MAX val inputSize: Long, // dont be greater than Int.MAX
val filename: String val filename: String
) { ) {
constructor(
width: Int,
height: Int,
md5: ByteArray,
imageFormat: String,
input: ByteReadChannel,
inputSize: Long, // dont be greater than Int.MAX
filename: String
) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
constructor(
width: Int,
height: Int,
md5: ByteArray,
imageFormat: String,
input: Input,
inputSize: Long, // dont be greater than Int.MAX
filename: String
) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
constructor(
width: Int,
height: Int,
md5: ByteArray,
imageFormat: String,
input: ByteReadPacket,
filename: String
) : this(width, height, md5, imageFormat, input as Any, input.remaining, filename)
constructor(
width: Int,
height: Int,
md5: ByteArray,
imageFormat: String,
input: InputStream,
filename: String
) : this(width, height, md5, imageFormat, input as Any, input.available().toLong(), filename)
init { init {
check(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" } require(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" }
} }
companion object { 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)
fun generateUUID(md5: ByteArray): String { fun generateUUID(md5: ByteArray): String {
return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}" return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}"
} }