From 5590ef510d21cf8959ed2c9d87ce6b568a201e45 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 23 Feb 2020 12:22:51 +0800 Subject: [PATCH] Powerful `ExternalImage`: support various types of input --- .../net/mamoe/mirai/qqandroid/ContactImpl.kt | 6 +- .../mirai/qqandroid/network/highway/Codec.kt | 36 +++++++-- .../network/highway/HighwayHelper.kt | 74 +++++++++++-------- .../net.mamoe.mirai/utils/ExternalImage.kt | 55 +++++++++++--- 4 files changed, 122 insertions(+), 49 deletions(-) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt index 8d1c88117..7920aee6b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.qqandroid import kotlinx.coroutines.launch +import kotlinx.io.core.Closeable import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.* import net.mamoe.mirai.event.broadcast @@ -40,6 +41,7 @@ internal abstract class ContactImpl : Contact { } override fun equals(other: Any?): Boolean { + @Suppress("DuplicatedCode") if (this === other) return true if (other !is Contact) return false if (this::class != other::class) return false @@ -144,7 +146,7 @@ internal class QQImpl( } } } finally { - image.input.close() + (image.input as? Closeable)?.close() } @MiraiExperimentalAPI @@ -642,7 +644,7 @@ internal class GroupImpl( } } } finally { - image.input.close() + (image.input as Closeable)?.close() } override fun toString(): String { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt index aee309aee..4aa0a88a5 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt @@ -9,13 +9,17 @@ package net.mamoe.mirai.qqandroid.network.highway +import io.ktor.utils.io.ByteReadChannel +import kotlinx.io.InputStream 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 object Highway { - fun RequestDataTrans( + suspend fun RequestDataTrans( uin: Long, command: String, sequenceId: Int, @@ -25,10 +29,11 @@ object Highway { localId: Int = 2052, uKey: ByteArray, - data: Input, + data: Any, dataSize: Int, md5: ByteArray ): 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(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}" } @@ -58,14 +63,15 @@ object Highway { } private object Codec { - fun buildC2SData( + suspend fun buildC2SData( dataHighwayHead: CSDataHighwayHead.DataHighwayHead, segHead: CSDataHighwayHead.SegHead, extendInfo: ByteArray, loginSigHead: CSDataHighwayHead.LoginSigHead?, - body: Input, + body: Any, bodySize: Int ): ByteReadPacket { + require(body is Input || body is InputStream || body is ByteReadChannel) { "unsupported body: ${body::class.simpleName}" } val head = CSDataHighwayHead.ReqDataHighwayHead( msgBasehead = dataHighwayHead, msgSeghead = segHead, @@ -78,7 +84,27 @@ object Highway { writeInt(head.size) writeInt(bodySize) 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) } } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt index adc5b372e..863728bb4 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt @@ -16,6 +16,9 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.URLProtocol import io.ktor.http.content.OutgoingContent 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.readAvailable import kotlinx.io.core.use @@ -35,47 +38,55 @@ internal suspend inline fun HttpClient.postImage( htcmd: String, uin: Long, groupcode: Long?, - imageInput: Input, + imageInput: Any, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor inputSize: Long, uKeyHex: String -): Boolean = try { - post { - url { - protocol = URLProtocol.HTTP - host = "htdata2.qq.com" - path("cgi-bin/httpconn") +): Boolean = post { + url { + protocol = URLProtocol.HTTP + host = "htdata2.qq.com" + path("cgi-bin/httpconn") - parameters["htcmd"] = htcmd - parameters["uin"] = uin.toString() + parameters["htcmd"] = htcmd + parameters["uin"] = uin.toString() - if (groupcode != null) parameters["groupcode"] = groupcode.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 + parameters["term"] = "pc" + parameters["ver"] = "5603" + parameters["filesize"] = inputSize.toString() + parameters["range"] = 0.toString() + parameters["ukey"] = uKeyHex - userAgent("QQClient") - } + userAgent("QQClient") + } - body = object : OutgoingContent.WriteChannelContent() { - override val contentType: ContentType = ContentType.Image.Any - override val contentLength: Long = inputSize + body = object : OutgoingContent.WriteChannelContent() { + override val contentType: ContentType = ContentType.Image.Any + override val contentLength: Long = inputSize - override suspend fun writeTo(channel: io.ktor.utils.io.ByteWriteChannel) { - ByteArrayPool.useInstance { buffer: ByteArray -> - var size: Int - while (imageInput.readAvailable(buffer).also { size = it } != 0) { - channel.writeFully(buffer, 0, size) + override suspend fun writeTo(channel: io.ktor.utils.io.ByteWriteChannel) { + ByteArrayPool.useInstance { buffer: ByteArray -> + when (imageInput) { + is Input -> { + 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 { - imageInput.close() -} + } +} == HttpStatusCode.OK @UseExperimental(MiraiInternalAPI::class) internal object HighwayHelper { @@ -84,11 +95,12 @@ internal object HighwayHelper { serverIp: String, serverPort: Int, uKey: ByteArray, - imageInput: Input, + imageInput: Any, inputSize: Int, md5: ByteArray, 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(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" } require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" } @@ -96,6 +108,8 @@ internal object HighwayHelper { val socket = PlatformSocket() socket.connect(serverIp, serverPort) socket.use { + + // TODO: 2020/2/23 使用缓存, 或使用 HTTP 发送更好 (因为无需读取到内存) socket.send( Highway.RequestDataTrans( uin = client.uin, diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt index 79ac12731..07ff6dbff 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt @@ -11,6 +11,8 @@ package net.mamoe.mirai.utils +import io.ktor.utils.io.ByteReadChannel +import kotlinx.io.InputStream import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.Input import net.mamoe.mirai.contact.Contact @@ -29,29 +31,58 @@ import net.mamoe.mirai.utils.io.toUHexString * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人 * @See ExternalImage.upload 上传图片并得到 [Image] 消息 */ -class ExternalImage( +class ExternalImage private constructor( val width: Int, val height: Int, val md5: ByteArray, 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 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 { - 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 { - 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 { return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}" }