mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-13 23:00:14 +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
|
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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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]}"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user