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
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 {

View File

@ -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)
}
}

View File

@ -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,11 +38,10 @@ 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<HttpStatusCode> {
): Boolean = post<HttpStatusCode> {
url {
protocol = URLProtocol.HTTP
host = "htdata2.qq.com"
@ -65,17 +67,26 @@ internal suspend inline fun HttpClient.postImage(
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)
}
}
} == HttpStatusCode.OK
} finally {
imageInput.close()
}
else -> error("unsupported imageInput: ${imageInput::class.simpleName}")
}
}
}
}
} == 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,

View File

@ -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
) {
init {
check(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" }
}
companion object {
operator fun invoke(
constructor(
width: Int,
height: Int,
md5: ByteArray,
format: String,
data: ByteReadPacket,
imageFormat: String,
input: ByteReadChannel,
inputSize: Long, // dont be greater than Int.MAX
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: 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 {
require(inputSize in 0L..Int.MAX_VALUE.toLong()) { "file is too big" }
}
companion object {
fun generateUUID(md5: ByteArray): String {
return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}"
}