Commit 5590ef51 authored by Him188's avatar Him188

Powerful `ExternalImage`: support various types of input

parent 6e2c8079
...@@ -10,6 +10,7 @@ ...@@ -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 { ...@@ -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( ...@@ -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( ...@@ -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 {
......
...@@ -9,13 +9,17 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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)
} }
} }
......
...@@ -16,6 +16,9 @@ import io.ktor.http.HttpStatusCode ...@@ -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,11 +38,10 @@ internal suspend inline fun HttpClient.postImage( ...@@ -35,11 +38,10 @@ 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"
...@@ -65,17 +67,26 @@ internal suspend inline fun HttpClient.postImage( ...@@ -65,17 +67,26 @@ internal suspend inline fun HttpClient.postImage(
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 ->
when (imageInput) {
is Input -> {
var size: Int var size: Int
while (imageInput.readAvailable(buffer).also { size = it } != 0) { while (imageInput.readAvailable(buffer).also { size = it } != 0) {
channel.writeFully(buffer, 0, size) 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)
} }
} }
} == HttpStatusCode.OK else -> error("unsupported imageInput: ${imageInput::class.simpleName}")
} finally { }
imageInput.close() }
} }
}
} == HttpStatusCode.OK
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
internal object HighwayHelper { internal object HighwayHelper {
...@@ -84,11 +95,12 @@ 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 { ...@@ -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,
......
...@@ -11,6 +11,8 @@ ...@@ -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 ...@@ -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
) { ) {
init { constructor(
check(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" } 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)
companion object { constructor(
operator fun invoke(
width: Int, width: Int,
height: Int, height: Int,
md5: ByteArray, md5: ByteArray,
format: String, imageFormat: String,
data: ByteReadPacket, input: Input,
inputSize: Long, // dont be greater than Int.MAX
filename: String filename: String
): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining, filename) ) : 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 {
require(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" }
}
companion object {
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]}"
} }
......
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