Commit 5b2ae6e9 authored by Him188's avatar Him188

Simplify Image structure, close #244

parent eb123bb3
...@@ -91,24 +91,14 @@ internal class FriendImpl( ...@@ -91,24 +91,14 @@ internal class FriendImpl(
fileId = 0, fileId = 0,
fileMd5 = image.md5, fileMd5 = image.md5,
fileSize = image.inputSize.toInt(), fileSize = image.inputSize.toInt(),
fileName = image.md5.toUHexString("") + "." + image.format, fileName = image.md5.toUHexString("") + ".gif",
imgOriginal = 1, imgOriginal = 1
imgWidth = image.width,
imgHeight = image.height,
imgType = image.imageType
) )
).sendAndExpect<LongConn.OffPicUp.Response>() ).sendAndExpect<LongConn.OffPicUp.Response>()
@Suppress("UNCHECKED_CAST") // bug @Suppress("UNCHECKED_CAST") // bug
return when (response) { return when (response) {
is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage( is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId).also {
filepath = response.resourceId,
md5 = response.imageInfo.fileMd5,
fileLength = response.imageInfo.fileSize.toInt(),
height = response.imageInfo.fileHeight,
width = response.imageInfo.fileWidth,
resourceId = response.resourceId
).also {
ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast() ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast()
} }
is LongConn.OffPicUp.Response.RequireUpload -> { is LongConn.OffPicUp.Response.RequireUpload -> {
...@@ -132,14 +122,7 @@ internal class FriendImpl( ...@@ -132,14 +122,7 @@ internal class FriendImpl(
//) //)
// 为什么不能 ?? // 为什么不能 ??
return OfflineFriendImage( return OfflineFriendImage(response.resourceId).also {
filepath = response.resourceId,
md5 = image.md5,
fileLength = image.inputSize.toInt(),
height = image.height,
width = image.width,
resourceId = response.resourceId
).also {
ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast() ImageUploadEvent.Succeed(this@FriendImpl, image, it).broadcast()
} }
} }
......
...@@ -400,11 +400,7 @@ internal class GroupImpl( ...@@ -400,11 +400,7 @@ internal class GroupImpl(
uin = bot.id, uin = bot.id,
groupCode = id, groupCode = id,
md5 = image.md5, md5 = image.md5,
size = image.inputSize, size = image.inputSize
picWidth = image.width,
picHeight = image.height,
picType = image.imageType,
filename = image.filename
).sendAndExpect() ).sendAndExpect()
@Suppress("UNCHECKED_CAST") // bug @Suppress("UNCHECKED_CAST") // bug
...@@ -427,10 +423,8 @@ internal class GroupImpl( ...@@ -427,10 +423,8 @@ internal class GroupImpl(
// fileId = response.fileId.toInt() // fileId = response.fileId.toInt()
// ) // )
// println("NMSL") // println("NMSL")
return OfflineGroupImage( return OfflineGroupImage(imageId = resourceId)
md5 = image.md5, .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
filepath = resourceId
).also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
} }
is ImgStore.GroupPicUp.Response.RequireUpload -> { is ImgStore.GroupPicUp.Response.RequireUpload -> {
// 每 10KB 等 1 秒, 最少等待 5 秒 // 每 10KB 等 1 秒, 最少等待 5 秒
...@@ -480,10 +474,8 @@ internal class GroupImpl( ...@@ -480,10 +474,8 @@ internal class GroupImpl(
// imageType = image.imageType, // imageType = image.imageType,
// fileId = response.fileId.toInt() // fileId = response.fileId.toInt()
// ) // )
return OfflineGroupImage( return OfflineGroupImage(imageId = resourceId)
md5 = image.md5, .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
filepath = resourceId
).also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
/* /*
fileId = response.fileId.toInt(), fileId = response.fileId.toInt(),
fileType = 0, // ? fileType = 0, // ?
......
...@@ -2,6 +2,7 @@ package net.mamoe.mirai.qqandroid.message ...@@ -2,6 +2,7 @@ package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.message.data.FriendFlashImage import net.mamoe.mirai.message.data.FriendFlashImage
import net.mamoe.mirai.message.data.GroupFlashImage import net.mamoe.mirai.message.data.GroupFlashImage
import net.mamoe.mirai.message.data.md5
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.HummerCommelem import net.mamoe.mirai.qqandroid.network.protocol.data.proto.HummerCommelem
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
...@@ -13,7 +14,7 @@ internal fun GroupFlashImage.toJceData() = ImMsgBody.Elem( ...@@ -13,7 +14,7 @@ internal fun GroupFlashImage.toJceData() = ImMsgBody.Elem(
businessType = 0, businessType = 0,
pbElem = HummerCommelem.MsgElemInfoServtype3( pbElem = HummerCommelem.MsgElemInfoServtype3(
flashTroopPic = ImMsgBody.CustomFace( flashTroopPic = ImMsgBody.CustomFace(
filePath = image.filepath, filePath = image.imageId,
md5 = image.md5, md5 = image.md5,
pbReserve = byteArrayOf(0x78, 0x06) pbReserve = byteArrayOf(0x78, 0x06)
) )
...@@ -27,9 +28,8 @@ internal fun FriendFlashImage.toJceData() = ImMsgBody.Elem( ...@@ -27,9 +28,8 @@ internal fun FriendFlashImage.toJceData() = ImMsgBody.Elem(
businessType = 0, businessType = 0,
pbElem = HummerCommelem.MsgElemInfoServtype3( pbElem = HummerCommelem.MsgElemInfoServtype3(
flashC2cPic = ImMsgBody.NotOnlineImage( flashC2cPic = ImMsgBody.NotOnlineImage(
filePath = image.filepath, filePath = image.imageId,
fileId = image.fileId, resId = image.imageId,
resId = image.resourceId,
picMd5 = image.md5, picMd5 = image.md5,
oldPicMd5 = false, oldPicMd5 = false,
pbReserve = byteArrayOf(0x78, 0x06) pbReserve = byteArrayOf(0x78, 0x06)
......
...@@ -9,10 +9,7 @@ ...@@ -9,10 +9,7 @@
package net.mamoe.mirai.qqandroid.message package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.message.data.OfflineFriendImage import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.OfflineGroupImage
import net.mamoe.mirai.message.data.OnlineFriendImage
import net.mamoe.mirai.message.data.OnlineGroupImage
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.utils.hexToBytes import net.mamoe.mirai.qqandroid.utils.hexToBytes
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
...@@ -20,28 +17,12 @@ import net.mamoe.mirai.utils.ExternalImage ...@@ -20,28 +17,12 @@ import net.mamoe.mirai.utils.ExternalImage
internal class OnlineGroupImageImpl( internal class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace internal val delegate: ImMsgBody.CustomFace
) : OnlineGroupImage() { ) : OnlineGroupImage() {
override val filepath: String = delegate.filePath override val imageId: String = ExternalImage.generateImageId(delegate.md5)
override val fileId: Int get() = delegate.fileId
override val serverIp: Int get() = delegate.serverIp
override val serverPort: Int get() = delegate.serverPort
override val fileType: Int get() = delegate.fileType
override val signature: ByteArray get() = delegate.signature
override val useful: Int get() = delegate.useful
override val md5: ByteArray get() = delegate.md5
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imageType
override val width: Int get() = delegate.width
override val height: Int get() = delegate.height
override val source: Int get() = delegate.source
override val size: Int get() = delegate.size
override val original: Int get() = delegate.origin
override val pbReserve: ByteArray get() = delegate.pbReserve
override val imageId: String = ExternalImage.generateImageId(delegate.md5, imageType)
override val originUrl: String override val originUrl: String
get() = "http://gchat.qpic.cn" + delegate.origUrl get() = "http://gchat.qpic.cn" + delegate.origUrl
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is OnlineGroupImageImpl && other.filepath == this.filepath && other.md5.contentEquals(this.md5) return other is OnlineGroupImageImpl && other.imageId == this.imageId
} }
override fun hashCode(): Int { override fun hashCode(): Int {
...@@ -52,23 +33,13 @@ internal class OnlineGroupImageImpl( ...@@ -52,23 +33,13 @@ internal class OnlineGroupImageImpl(
internal class OnlineFriendImageImpl( internal class OnlineFriendImageImpl(
internal val delegate: ImMsgBody.NotOnlineImage internal val delegate: ImMsgBody.NotOnlineImage
) : OnlineFriendImage() { ) : OnlineFriendImage() {
override val resourceId: String get() = delegate.resId override val imageId: String get() = delegate.resId
override val md5: ByteArray get() = delegate.picMd5
override val filepath: String get() = delegate.filePath
override val fileLength: Int get() = delegate.fileLen
override val height: Int get() = delegate.picHeight
override val width: Int get() = delegate.picWidth
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imgType
override val downloadPath: String get() = delegate.downloadPath
override val fileId: Int get() = delegate.fileId
override val original: Int get() = delegate.original override val original: Int get() = delegate.original
override val originUrl: String override val originUrl: String
get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is OnlineFriendImageImpl && other.resourceId == this.resourceId && other.md5 return other is OnlineFriendImageImpl && other.imageId == this.imageId
.contentEquals(this.md5)
} }
override fun hashCode(): Int { override fun hashCode(): Int {
...@@ -78,22 +49,8 @@ internal class OnlineFriendImageImpl( ...@@ -78,22 +49,8 @@ internal class OnlineFriendImageImpl(
internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace { internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace( return ImMsgBody.CustomFace(
filePath = this.filepath, filePath = this.imageId,
fileId = this.fileId,
serverIp = this.serverIp,
serverPort = this.serverPort,
fileType = this.fileType,
signature = this.signature,
useful = this.useful,
md5 = this.md5, md5 = this.md5,
bizType = this.bizType,
imageType = this.imageType,
width = this.width,
height = this.height,
source = this.source,
size = this.size,
origin = this.original,
pbReserve = this.pbReserve,
flag = ByteArray(4), flag = ByteArray(4),
//_400Height = 235, //_400Height = 235,
//_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2", //_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
...@@ -108,18 +65,12 @@ private val oldData: ByteArray = ...@@ -108,18 +65,12 @@ private val oldData: ByteArray =
internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage { internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
return ImMsgBody.NotOnlineImage( return ImMsgBody.NotOnlineImage(
filePath = this.filepath, filePath = this.imageId,
resId = this.resourceId, resId = this.imageId,
oldPicMd5 = false, oldPicMd5 = false,
picMd5 = this.md5, picMd5 = this.md5,
fileLen = this.fileLength, downloadPath = this.imageId,
picHeight = this.height, original = 1,
picWidth = this.width,
bizType = this.bizType,
imgType = this.imageType,
downloadPath = this.downloadPath,
original = this.original,
fileId = this.fileId,
pbReserve = byteArrayOf(0x78, 0x02) pbReserve = byteArrayOf(0x78, 0x02)
) )
} }
\ No newline at end of file
...@@ -28,7 +28,6 @@ import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH ...@@ -28,7 +28,6 @@ import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH
import net.mamoe.mirai.qqandroid.utils.cryptor.TEA import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt
internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddress) internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddress)
...@@ -43,7 +42,7 @@ private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray ...@@ -43,7 +42,7 @@ private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray
/** /**
* 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray] * 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray]
*/ */
internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0..255).toByte() } internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0, 255).toByte() }
internal object DefaultServerList : Set<Pair<String, Int>> by setOf( internal object DefaultServerList : Set<Pair<String, Int>> by setOf(
"42.81.169.46" to 8080, "42.81.169.46" to 8080,
......
...@@ -129,8 +129,8 @@ internal class Cmd0x352 : ProtoBuf { ...@@ -129,8 +129,8 @@ internal class Cmd0x352 : ProtoBuf {
@ProtoId(11) @JvmField val retry: Int = 0,//default @ProtoId(11) @JvmField val retry: Int = 0,//default
@ProtoId(12) @JvmField val buType: Int = 1,//1或96 不确定 @ProtoId(12) @JvmField val buType: Int = 1,//1或96 不确定
@ProtoId(13) @JvmField val imgOriginal: Int,//是否为原图 @ProtoId(13) @JvmField val imgOriginal: Int,//是否为原图
@ProtoId(14) @JvmField val imgWidth: Int, @ProtoId(14) @JvmField val imgWidth: Int = 0,
@ProtoId(15) @JvmField val imgHeight: Int, @ProtoId(15) @JvmField val imgHeight: Int = 0,
/** /**
* ImgType: * ImgType:
* JPG: 1000 * JPG: 1000
......
...@@ -19,6 +19,19 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory ...@@ -19,6 +19,19 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
import kotlin.random.Random
import kotlin.random.nextInt
internal fun getRandomString(length: Int): String =
getRandomString(length, *defaultRanges)
private val defaultRanges: Array<CharRange> = arrayOf('a'..'z', 'A'..'Z', '0'..'9')
internal fun getRandomString(length: Int, charRange: CharRange): String =
String(CharArray(length) { charRange.random() })
internal fun getRandomString(length: Int, vararg charRanges: CharRange): String =
String(CharArray(length) { charRanges[Random.Default.nextInt(0..charRanges.lastIndex)].random() })
internal class ImgStore { internal class ImgStore {
object GroupPicUp : OutgoingPacketFactory<GroupPicUp.Response>("ImgStore.GroupPicUp") { object GroupPicUp : OutgoingPacketFactory<GroupPicUp.Response>("ImgStore.GroupPicUp") {
...@@ -33,7 +46,7 @@ internal class ImgStore { ...@@ -33,7 +46,7 @@ internal class ImgStore {
picHeight: Int = 0, // not orthodox picHeight: Int = 0, // not orthodox
picType: Int = 1000, picType: Int = 1000,
fileId: Long = 0, fileId: Long = 0,
filename: String, filename: String = getRandomString(16) + ".gif", // make server happier
srcTerm: Int = 5, srcTerm: Int = 5,
platformType: Int = 9, platformType: Int = 9,
buType: Int = 1, buType: Int = 1,
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotImpl import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.js.JsName import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
...@@ -65,7 +65,18 @@ expect interface Image : Message, MessageContent { ...@@ -65,7 +65,18 @@ expect interface Image : Message, MessageContent {
@Suppress("FunctionName") @Suppress("FunctionName")
@JsName("newImage") @JsName("newImage")
@JvmName("newImage") @JvmName("newImage")
fun Image(imageId: String): Image = when { fun Image(imageId: String): OfflineImage = when {
imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE")
}
@JvmSynthetic
@Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN)
@Suppress("FunctionName")
@JsName("newImage")
@JvmName("newImage")
fun Image2(imageId: String): Image = when {
imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE") else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE")
...@@ -100,8 +111,7 @@ sealed class AbstractImage : Image { ...@@ -100,8 +111,7 @@ sealed class AbstractImage : Image {
*/ */
interface OnlineImage : Image { interface OnlineImage : Image {
companion object Key : Message.Key<OnlineImage> { companion object Key : Message.Key<OnlineImage> {
override val typeName: String override val typeName: String get() = "OnlineImage"
get() = "OnlineImage"
} }
/** /**
...@@ -113,6 +123,7 @@ interface OnlineImage : Image { ...@@ -113,6 +123,7 @@ interface OnlineImage : Image {
/** /**
* 查询原图下载链接. * 查询原图下载链接.
*/ */
@JvmSynthetic
suspend fun Image.queryUrl(): String { suspend fun Image.queryUrl(): String {
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
return when (this) { return when (this) {
...@@ -135,8 +146,7 @@ suspend fun Image.queryUrl(): String { ...@@ -135,8 +146,7 @@ suspend fun Image.queryUrl(): String {
*/ */
interface OfflineImage : Image { interface OfflineImage : Image {
companion object Key : Message.Key<OfflineImage> { companion object Key : Message.Key<OfflineImage> {
override val typeName: String override val typeName: String get() = "OfflineImage"
get() = "OfflineImage"
} }
} }
...@@ -163,26 +173,8 @@ suspend fun OfflineImage.queryUrl(): String { ...@@ -163,26 +173,8 @@ suspend fun OfflineImage.queryUrl(): String {
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
sealed class GroupImage : AbstractImage() { sealed class GroupImage : AbstractImage() {
companion object Key : Message.Key<GroupImage> { companion object Key : Message.Key<GroupImage> {
override val typeName: String override val typeName: String get() = "GroupImage"
get() = "GroupImage"
} }
abstract val filepath: String
abstract val fileId: Int
abstract val serverIp: Int
abstract val serverPort: Int
abstract val fileType: Int
abstract val signature: ByteArray
abstract val useful: Int
abstract val md5: ByteArray
abstract val bizType: Int
abstract val imageType: Int
abstract val width: Int
abstract val height: Int
abstract val source: Int
abstract val size: Int
abstract val pbReserve: ByteArray
abstract val original: Int
} }
/** /**
...@@ -190,35 +182,13 @@ sealed class GroupImage : AbstractImage() { ...@@ -190,35 +182,13 @@ sealed class GroupImage : AbstractImage() {
*/ */
@Serializable @Serializable
data class OfflineGroupImage( data class OfflineGroupImage(
override val filepath: String, // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png override val imageId: String
override val md5: ByteArray ) : GroupImage(), OfflineImage
) : GroupImage(), OfflineImage {
constructor(imageId: String) : this(filepath = imageId, md5 = calculateImageMd5ByImageId(imageId))
override val fileId: Int get() = 0
override val serverIp: Int get() = 0
override val serverPort: Int get() = 0
override val fileType: Int get() = 0 // 0
override val signature: ByteArray get() = EMPTY_BYTE_ARRAY
override val useful: Int get() = 1
override val bizType: Int get() = 0
override val imageType: Int get() = 0
override val width: Int get() = 0
override val height: Int get() = 0
override val source: Int get() = 200
override val size: Int get() = 0
override val original: Int get() = 1
override val pbReserve: ByteArray get() = EMPTY_BYTE_ARRAY
override val imageId: String get() = filepath
override fun hashCode(): Int {
return filepath.hashCode() + 31 * md5.hashCode()
}
override fun equals(other: Any?): Boolean { @get:JvmName("calculateImageMd5")
return other is OfflineGroupImage && other::class == this::class && other.md5.contentEquals(this.md5) && other.filepath == this.filepath @SinceMirai("0.39.0")
} val Image.md5: ByteArray
} get() = calculateImageMd5ByImageId(imageId)
/** /**
* 接收消息时获取到的 [GroupImage]. 它可以直接获取下载链接 [originUrl] * 接收消息时获取到的 [GroupImage]. 它可以直接获取下载链接 [originUrl]
...@@ -239,51 +209,19 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage ...@@ -239,51 +209,19 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
sealed class FriendImage : AbstractImage() { sealed class FriendImage : AbstractImage() {
companion object Key : Message.Key<FriendImage> { companion object Key : Message.Key<FriendImage> {
override val typeName: String override val typeName: String get() = "FriendImage"
get() = "FriendImage"
} }
abstract val resourceId: String
abstract val md5: ByteArray
abstract val filepath: String
abstract val fileLength: Int
abstract val height: Int
abstract val width: Int
open val bizType: Int get() = 0
open val imageType: Int get() = 1000
abstract val fileId: Int
open val downloadPath: String get() = resourceId
open val original: Int get() = 1 open val original: Int get() = 1
override val imageId: String get() = resourceId
} }
/** /**
* 通过 [Group.uploadImage] 上传得到的 [GroupImage]. 它的链接需要查询 [Bot.queryImageUrl] * 通过 [Group.uploadImage] 上传得到的 [GroupImage]. 它的链接需要查询 [Bot.queryImageUrl]
*/ */
@Serializable
data class OfflineFriendImage( data class OfflineFriendImage(
override val resourceId: String, override val imageId: String
override val md5: ByteArray, ) : FriendImage(), OfflineImage
@Transient override val filepath: String = resourceId,
@Transient override val fileLength: Int = 0,
@Transient override val height: Int = 0,
@Transient override val width: Int = 0,
@Transient override val bizType: Int = 0,
@Transient override val imageType: Int = 1000,
@Transient override val downloadPath: String = resourceId,
@Transient override val fileId: Int = 0
) : FriendImage(), OfflineImage {
constructor(imageId: String) : this(resourceId = imageId, md5 = calculateImageMd5ByImageId(imageId))
override fun hashCode(): Int {
return resourceId.hashCode() + 31 * md5.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is OfflineFriendImage && other::class == this::class &&
other.md5.contentEquals(this.md5) && other.resourceId == this.resourceId
}
}
/** /**
* 接收消息时获取到的 [FriendImage]. 它可以直接获取下载链接 [originUrl] * 接收消息时获取到的 [FriendImage]. 它可以直接获取下载链接 [originUrl]
......
...@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.MessageReceipt ...@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.OfflineImage import net.mamoe.mirai.message.data.OfflineImage
import net.mamoe.mirai.message.data.sendTo import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.message.data.toLongUnsigned
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
/** /**
...@@ -34,52 +35,32 @@ import kotlin.jvm.JvmSynthetic ...@@ -34,52 +35,32 @@ import kotlin.jvm.JvmSynthetic
* @See ExternalImage.upload 上传图片并得到 [Image] 消息 * @See ExternalImage.upload 上传图片并得到 [Image] 消息
*/ */
class ExternalImage private constructor( class ExternalImage private constructor(
val width: Int,
val height: Int,
val md5: ByteArray, val md5: ByteArray,
imageFormat: String,
val input: Any, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor 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
) { ) {
constructor( constructor(
width: Int,
height: Int,
md5: ByteArray, md5: ByteArray,
imageFormat: String,
input: ByteReadChannel, input: ByteReadChannel,
inputSize: Long, // dont be greater than Int.MAX inputSize: Long // dont be greater than Int.MAX
filename: String ) : this(md5, input as Any, inputSize)
) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
constructor( constructor(
width: Int,
height: Int,
md5: ByteArray, md5: ByteArray,
imageFormat: String,
input: Input, input: Input,
inputSize: Long, // dont be greater than Int.MAX inputSize: Long // dont be greater than Int.MAX
filename: String ) : this(md5, input as Any, inputSize)
) : this(width, height, md5, imageFormat, input as Any, inputSize, filename)
constructor( constructor(
width: Int,
height: Int,
md5: ByteArray, md5: ByteArray,
imageFormat: String, input: ByteReadPacket
input: ByteReadPacket, ) : this(md5, input as Any, input.remaining)
filename: String
) : this(width, height, md5, imageFormat, input as Any, input.remaining, filename)
@OptIn(InternalSerializationApi::class) @OptIn(InternalSerializationApi::class)
constructor( constructor(
width: Int,
height: Int,
md5: ByteArray, md5: ByteArray,
imageFormat: String, input: InputStream
input: InputStream, ) : this(md5, input as Any, input.available().toLongUnsigned())
filename: String
) : this(width, height, md5, imageFormat, input as Any, input.available().toLong(), filename)
init { init {
require(inputSize < 30L * 1024 * 1024) { "file is too big. Maximum is about 20MB" } require(inputSize < 30L * 1024 * 1024) { "file is too big. Maximum is about 20MB" }
...@@ -87,63 +68,30 @@ class ExternalImage private constructor( ...@@ -87,63 +68,30 @@ class ExternalImage private constructor(
companion object { 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]}"
} }
fun generateImageId(md5: ByteArray, imageType: Int): String { fun generateImageId(md5: ByteArray): String {
return """{${generateUUID(md5)}}.${determineFormat(imageType)}""" return """{${generateUUID(md5)}}.gif"""
}
fun determineImageType(format: String): Int {
return when (format) {
"jpg" -> 1000
"png" -> 1001
"webp" -> 1002
"bmp" -> 1005
"gig" -> 2000
"apng" -> 2001
"sharpp" -> 1004
else -> 1000 // unsupported, just make it jpg
}
}
fun determineFormat(imageType: Int): String {
return when (imageType) {
1000 -> "jpg"
1001 -> "png"
1002 -> "webp"
1005 -> "bmp"
2000 -> "gig"
2001 -> "apng"
1004 -> "sharpp"
else -> "jpg" // unsupported, just make it jpg
}
} }
} }
val format: String = /*
when (val it = imageFormat.toLowerCase()) {
"jpeg" -> "jpg" //必须转换
else -> it
}
/**
* ImgType: * ImgType:
* JPG: 1000 * JPG: 1000
* PNG: 1001 * PNG: 1001
* WEBP: 1002 * WEBP: 1002
* BMP: 1005 * BMP: 1005
* GIG: 2000 // TODO gig? gif? * GIG: 2000 // gig? gif?
* APNG: 2001 * APNG: 2001
* SHARPP: 1004 * SHARPP: 1004
*/ */
val imageType: Int
get() = determineImageType(format)
override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
override fun toString(): String = "[ExternalImage(${generateUUID(md5)})]"
fun calculateImageResourceId(): String { fun calculateImageResourceId(): String {
return "{${generateUUID(md5)}}.$format" return "{${generateUUID(md5)}}.gif"
} }
} }
...@@ -176,8 +124,8 @@ suspend fun ExternalImage.upload(contact: Contact): OfflineImage = when (contact ...@@ -176,8 +124,8 @@ suspend fun ExternalImage.upload(contact: Contact): OfflineImage = when (contact
@JvmSynthetic @JvmSynthetic
suspend inline fun <C : Contact> C.sendImage(image: ExternalImage): MessageReceipt<C> = image.sendTo(this) suspend inline fun <C : Contact> C.sendImage(image: ExternalImage): MessageReceipt<C> = image.sendTo(this)
internal operator fun ByteArray.get(range: IntRange): String = buildString { internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString {
range.forEach { for (it in rangeStart..rangeEnd) {
append(this@get[it].fixToString()) append(this@get[it].fixToString())
} }
} }
......
...@@ -7,8 +7,8 @@ internal class ExternalImageTest { ...@@ -7,8 +7,8 @@ internal class ExternalImageTest {
@Test @Test
fun testByteArrayGet() { fun testByteArrayGet() {
assertEquals("0F", byteArrayOf(0x0f)[0..0]) assertEquals("0F", byteArrayOf(0x0f)[0, 0])
assertEquals("10", byteArrayOf(0x10)[0..0]) assertEquals("10", byteArrayOf(0x10)[0, 0])
assertEquals("0FFE", byteArrayOf(0x0F, 0xFE.toByte())[0..1]) assertEquals("0FFE", byteArrayOf(0x0F, 0xFE.toByte())[0, 1])
} }
} }
\ No newline at end of file
...@@ -43,8 +43,7 @@ import java.net.URL ...@@ -43,8 +43,7 @@ import java.net.URL
*/ */
actual interface Image : Message, MessageContent { actual interface Image : Message, MessageContent {
actual companion object Key : Message.Key<Image> { actual companion object Key : Message.Key<Image> {
actual override val typeName: String actual override val typeName: String get() = "Image"
get() = "Image"
} }
/** /**
......
...@@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers.IO ...@@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.io.ByteReadChannel import kotlinx.coroutines.io.ByteReadChannel
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.io.core.Input import kotlinx.io.core.Input
import kotlinx.io.core.buildPacket
import kotlinx.io.core.copyTo import kotlinx.io.core.copyTo
import kotlinx.io.errors.IOException import kotlinx.io.errors.IOException
import kotlinx.io.streams.asOutput import kotlinx.io.streams.asOutput
...@@ -34,49 +33,49 @@ import javax.imageio.ImageIO ...@@ -34,49 +33,49 @@ import javax.imageio.ImageIO
/** /**
* 读取 [BufferedImage] 的属性, 然后构造 [ExternalImage] * 将 [BufferedImage] 保存稳临时文件, 然后构造 [ExternalImage]
*/ */
@JvmOverloads @JvmOverloads
@Throws(IOException::class) @Throws(IOException::class)
fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage { fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
val digest = MessageDigest.getInstance("md5") val digest = MessageDigest.getInstance("md5")
digest.reset() digest.reset()
val buffer = buildPacket { file.outputStream().use { out ->
ImageIO.write(this@toExternalImage, formatName, object : OutputStream() { ImageIO.write(this@toExternalImage, formatName, object : OutputStream() {
override fun write(b: Int) { override fun write(b: Int) {
b.toByte().let { out.write(b)
this@buildPacket.writeByte(it) digest.update(b.toByte())
digest.update(it) }
}
override fun write(b: ByteArray) {
out.write(b)
digest.update(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
out.write(b, off, len)
digest.update(b, off, len)
} }
}) })
} }
return ExternalImage(width, height, digest.digest(), formatName, buffer, getRandomString(16) + "." + formatName) return ExternalImage(digest.digest(), file.inputStream())
} }
suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/** /**
* 读取文件头识别图片属性, 然后构造 [ExternalImage] * 直接使用文件 [inputStream] 构造 [ExternalImage]
*/ */
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
@Throws(IOException::class) @Throws(IOException::class)
fun File.toExternalImage(): ExternalImage { fun File.toExternalImage(): ExternalImage {
val input = ImageIO.createImageInputStream(this)
checkNotNull(input) { "Unable to read file(path=${this.path}), no ImageInputStream found" }
val image = ImageIO.getImageReaders(input).asSequence().firstOrNull()
?: error("Unable to read file(path=${this.path}), no ImageReader found (file type not supported)")
image.input = input
return ExternalImage( return ExternalImage(
width = image.getWidth(0),
height = image.getHeight(0),
md5 = this.inputStream().md5(), // dont change md5 = this.inputStream().md5(), // dont change
imageFormat = image.formatName, input = this.inputStream()
input = this.inputStream(),
filename = this.name
) )
} }
......
...@@ -87,7 +87,9 @@ internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Ra ...@@ -87,7 +87,9 @@ internal fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Ra
* 随机生成长度为 [length] 的 [String]. * 随机生成长度为 [length] 的 [String].
*/ */
internal fun getRandomString(length: Int): String = internal fun getRandomString(length: Int): String =
getRandomString(length, 'a'..'z', 'A'..'Z', '0'..'9') getRandomString(length, *defaultRanges)
private val defaultRanges: Array<CharRange> = arrayOf('a'..'z', 'A'..'Z', '0'..'9')
/** /**
* 根据所给 [charRange] 随机生成长度为 [length] 的 [String]. * 根据所给 [charRange] 随机生成长度为 [length] 的 [String].
......
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