mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-13 14:50:43 +08:00
Powerful ExternalImage
: support various types of input
This commit is contained in:
parent
6e2c8079ac
commit
5590ef510d
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<HttpStatusCode> {
|
||||
url {
|
||||
protocol = URLProtocol.HTTP
|
||||
host = "htdata2.qq.com"
|
||||
path("cgi-bin/httpconn")
|
||||
): Boolean = post<HttpStatusCode> {
|
||||
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,
|
||||
|
@ -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]}"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user