Commit 0a471e9b authored by Him188's avatar Him188

Add extensions for images

parent 47ece90d
package net.mamoe.mirai
/**
* Mirai 的一些信息.
*
* @see MiraiEnvironment 环境信息
*/
object Mirai {
const val VERSION: String = "1.0.0"
}
\ No newline at end of file
package net.mamoe.mirai
expect object MiraiEnvironment
\ No newline at end of file
......@@ -26,12 +26,12 @@ sealed class Contact(val bot: Bot, val id: UInt) {
abstract suspend fun sendMessage(message: MessageChain)
suspend fun sendMessage(message: Message) = sendMessage(message.toChain())
suspend fun sendMessage(plain: String) = sendMessage(PlainText(plain))
abstract suspend fun sendXMLMessage(message: String)
}
suspend fun Contact.sendMessage(plain: String) = sendMessage(PlainText(plain))
suspend fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
/**
* 一般的用户可见的 ID.
......
......@@ -2,6 +2,7 @@ package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.MessageChain
......
......@@ -3,6 +3,7 @@ package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.network.protocol.tim.packet.event.SenderPermission
......
......@@ -4,6 +4,7 @@ package net.mamoe.mirai.message
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket
import net.mamoe.mirai.utils.ExternalImage
......@@ -78,6 +79,11 @@ interface Message {
infix operator fun plus(another: Number): MessageChain = this.concat(another.toString().toMessage())
}
/**
* 将 [this] 发送给指定联系人
*/
suspend fun Message.sendTo(contact: Contact) = contact.sendMessage(this)
// ==================================== PlainText ====================================
inline class PlainText(override val stringValue: String) : Message {
......@@ -91,8 +97,6 @@ inline class PlainText(override val stringValue: String) : Message {
* 由接收消息时构建, 可直接发送
*
* @param id 这个图片的 [ImageId]
*
* @see
*/
inline class Image(val id: ImageId) : Message {
override val stringValue: String get() = "[${id.value}]"
......@@ -108,6 +112,10 @@ inline class Image(val id: ImageId) : Message {
*/
inline class ImageId(val value: String)
fun ImageId.image(): Image = Image(this)
suspend fun ImageId.sendTo(contact: Contact) = contact.sendMessage(this.image())
// ==================================== At ====================================
/**
......
......@@ -10,23 +10,25 @@ import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
import net.mamoe.mirai.network.protocol.tim.packet.ResponsePacket
import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImageIdRequestPacket.Response.State.*
import net.mamoe.mirai.network.qqAccount
import net.mamoe.mirai.network.session
import net.mamoe.mirai.qqAccount
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.httpPostFriendImage
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.readUnsignedVarInt
import net.mamoe.mirai.utils.writeUVarInt
import net.mamoe.mirai.withSession
/**
* 上传图片
* 挂起直到上传完成或失败
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
*/
suspend fun QQ.uploadImage(image: ExternalImage): ImageId = with(bot.network.session) {
//SubmitImageFilenamePacket(account, account, "sdiovaoidsa.png", sessionKey).sendAndExpect<ServerSubmitImageFilenameResponsePacket>().join()
DebugLogger.logPurple("正在上传好友图片, md5=${image.md5.toUHexString()}")
return FriendImageIdRequestPacket(this.qqAccount, sessionKey, id, image).sendAndExpect<FriendImageIdRequestPacket.Response, ImageId> {
if (it.uKey != null)
suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {
FriendImageIdRequestPacket(qqAccount, sessionKey, id, image).sendAndExpect<FriendImageIdRequestPacket.Response, ImageId> {
when (it.state) {
REQUIRE_UPLOAD -> {
require(
httpPostFriendImage(
botAccount = bot.qqAccount,
......@@ -35,6 +37,17 @@ suspend fun QQ.uploadImage(image: ExternalImage): ImageId = with(bot.network.ses
inputSize = image.inputSize
)
)
}
ALREADY_EXISTS -> {
}
OVER_FILE_SIZE_MAX -> {
throw OverFileSizeMaxException()
}
}
it.imageId!!
}.await()
}
......@@ -110,7 +123,7 @@ class SubmitImageFilenamePacket(
@PacketId(0x03_52u)
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2.21173")
class FriendImageIdRequestPacket(
private val botNumber: UInt,
private val bot: UInt,
private val sessionKey: ByteArray,
private val target: UInt,
private val image: ExternalImage
......@@ -119,7 +132,7 @@ class FriendImageIdRequestPacket(
//00 00 00 07 00 00 00 4B 08 01 12 03 98 01 01 08 01 12 47 08 A2 FF 8C F0 03 10 89 FC A6 8C 0B 18 00 22 10 2B 23 D7 05 CA D1 F2 CF 37 10 FE 58 26 92 FC C4 28 FD 08 32 1A 7B 00 47 00 47 00 42 00 7E 00 49 00 31 00 5A 00 4D 00 43 00 28 00 25 00 49 00 38 01 48 00 70 42 78 42
override fun encode(builder: BytePacketBuilder) = with(builder) {
writeQQ(botNumber)
writeQQ(bot)
//04 00 00 00 01 01 01 00 00 68 20 00 00 00 00 00 00 00 00
writeHex("04 00 00 00 01 2E 01 00 00 69 35 00 00 00 00 00 00 00 00")
......@@ -209,7 +222,7 @@ class FriendImageIdRequestPacket(
writeUVarintLVPacket(tag = 0x12u, lengthOffset = { it + 1 }) {
writeUByte(0x08u)
writeUVarInt(botNumber)
writeUVarInt(bot)
writeUByte(0x10u)
writeUVarInt(target)
......@@ -321,11 +334,11 @@ class FriendImageIdRequestPacket(
//83 12 06 98 01 01 A0 01 00 08 01 12 7D 08 00 10 9B A4 DC 92 06 18 00 28 01 32 1B 0A 10 8E C4 9D 72 26 AE 20 C0 5D A2 B6 78 4D 12 B7 3A 10 00 18 86 1F 20 30 28 30 52 25 2F 30 31 62
val toDiscard = readUByte().toInt() - 37
if (toDiscard < 0) {
state = State.OVER_FILE_SIZE_MAX
state = OVER_FILE_SIZE_MAX
} else {
discardExact(toDiscard)
imageId = ImageId(readString(37))
state = State.ALREADY_EXISTS
state = ALREADY_EXISTS
}
}
}
......
......@@ -7,6 +7,7 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.GroupId
import net.mamoe.mirai.contact.GroupInternalId
import net.mamoe.mirai.contact.withSession
import net.mamoe.mirai.message.ImageId
import net.mamoe.mirai.network.protocol.tim.packet.OutgoingPacket
import net.mamoe.mirai.network.protocol.tim.packet.PacketId
import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
......@@ -25,11 +26,9 @@ class OverFileSizeMaxException : IllegalStateException()
/**
* 上传群图片
* 挂起直到上传完成或失败
* 失败后抛出 [OverFileSizeMaxException]
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
*/
suspend fun Group.uploadImage(
image: ExternalImage
) = withSession {
suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
GroupImageIdRequestPacket(bot.qqAccount, internalId, image, sessionKey)
.sendAndExpect<GroupImageIdRequestPacket.Response, Unit> {
when (it.state) {
......@@ -50,6 +49,7 @@ suspend fun Group.uploadImage(
GroupImageIdRequestPacket.Response.State.OVER_FILE_SIZE_MAX -> throw OverFileSizeMaxException()
}
}.join()
image.groupImageId
}
/**
......
package net.mamoe.mirai.utils
import kotlinx.io.pool.DefaultPool
import kotlinx.io.pool.ObjectPool
internal const val DEFAULT_BUFFER_SIZE = 4098
internal const val DEFAULT_BYTE_ARRAY_POOL_SIZE = 2048
/**
* The default ktor byte buffer pool
*/
val ByteArrayPool: ObjectPool<ByteArray> = ByteBufferPool()
class ByteBufferPool : DefaultPool<ByteArray>(DEFAULT_BYTE_ARRAY_POOL_SIZE) {
override fun produceInstance(): ByteArray = ByteArray(DEFAULT_BUFFER_SIZE)
override fun clearInstance(instance: ByteArray): ByteArray = instance.apply { map { 0 } }
}
......@@ -4,8 +4,14 @@ package net.mamoe.mirai.utils
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Input
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.ImageId
import net.mamoe.mirai.message.sendTo
import net.mamoe.mirai.network.protocol.tim.packet.action.uploadImage
@Suppress("FunctionName")
fun ExternalImage(
width: Int,
height: Int,
......@@ -14,6 +20,10 @@ fun ExternalImage(
data: ByteReadPacket
) = ExternalImage(width, height, md5, format, data, data.remaining)
/**
* 外部图片. 图片数据还没有读取到内存.
* @see ExternalImage.sendTo
*/
class ExternalImage(
val width: Int,
val height: Int,
......@@ -40,6 +50,19 @@ class ExternalImage(
override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
}
/**
* 将图片发送给指定联系人
*/
suspend fun ExternalImage.sendTo(contact: Contact) = when (contact) {
is Group -> contact.uploadImage(this).sendTo(contact)
is QQ -> contact.uploadImage(this).sendTo(contact)
}
/**
* 将图片发送给 [this]
*/
suspend fun Contact.sendMessage(image: ExternalImage) = image.sendTo(this)
private operator fun ByteArray.get(range: IntRange): String = buildString {
range.forEach {
append(this@get[it].toUHexString())
......
......@@ -54,6 +54,7 @@ fun String.hexToUBytes(): UByteArray = HexCache.hexToUBytes(this)
fun String.hexToInt(): Int = hexToBytes().toUInt().toInt()
fun getRandomByteArray(length: Int): ByteArray = ByteArray(length) { Random.nextInt(0..255).toByte() }
fun getRandomString(length: Int): String = getRandomString(length, 'a'..'z', 'A'..'Z', '0'..'9')
fun getRandomString(length: Int, charRange: CharRange): String = String(CharArray(length) { charRange.random() })
fun getRandomString(length: Int, vararg charRanges: CharRange): String = String(CharArray(length) { charRanges[Random.Default.nextInt(0..charRanges.lastIndex)].random() })
fun ByteArray.toUInt(): UInt = this[0].toUInt().and(255u).shl(24) + this[1].toUInt().and(255u).shl(16) + this[2].toUInt().and(255u).shl(8) + this[3].toUInt().and(255u).shl(0)
......
@file:Suppress("MayBeConstant", "unused")
package net.mamoe.mirai
import java.io.File
actual typealias MiraiEnvironment = MiraiEnvironmentJvm
object MiraiEnvironmentJvm {
/**
* JVM only, 临时文件夹
*/
val TEMP_DIR: File = createTempDir().apply { deleteOnExit() }
}
\ No newline at end of file
@file:Suppress("unused")
package net.mamoe.mirai.message
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.core.Input
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.network.protocol.tim.packet.action.OverFileSizeMaxException
import net.mamoe.mirai.utils.sendTo
import net.mamoe.mirai.utils.toExternalImage
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
import java.net.URL
/*
* 发送图片的一些扩展函数.
*/
// region Type extensions
/**
* 将图片发送到指定联系人. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun BufferedImage.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* 下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun URL.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* 读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Input.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun InputStream.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* 将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun File.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
// endregion
// region Contact extensions
/**
* 将图片发送到指定联系人. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(bufferedImage: BufferedImage) = bufferedImage.sendAsImageTo(this)
/**
* 下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(imageUrl: URL) = imageUrl.sendAsImageTo(this)
/**
* 读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(imageInput: Input) = imageInput.sendAsImageTo(this)
/**
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(imageStream: InputStream) = imageStream.sendAsImageTo(this)
/**
* 将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(file: File) = file.sendAsImageTo(this)
// endregion
\ No newline at end of file
......@@ -2,16 +2,31 @@
package net.mamoe.mirai.utils
import io.ktor.util.asStream
import kotlinx.io.core.Input
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.buildPacket
import kotlinx.io.errors.IOException
import kotlinx.io.streams.asInput
import java.awt.image.BufferedImage
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import java.security.MessageDigest
import javax.imageio.ImageIO
import java.awt.image.BufferedImage as JavaBufferedImage
fun JavaBufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
/*
* 将各类型图片容器转为 [ExternalImage]
*/
/**
* 读取 [BufferedImage] 的属性, 然后构造 [ExternalImage]
*/
@Throws(IOException::class)
fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
val digest = MessageDigest.getInstance("md5")
digest.reset()
......@@ -29,6 +44,10 @@ fun JavaBufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage
return ExternalImage(width, height, digest.digest(), formatName, buffer)
}
/**
* 读取文件头识别图片属性, 然后构造 [ExternalImage]
*/
@Throws(IOException::class)
fun File.toExternalImage(): ExternalImage {
val input = ImageIO.createImageInputStream(this)
val image = ImageIO.getImageReaders(input).asSequence().firstOrNull() ?: error("Unable to read file(${this.path}), no ImageReader found")
......@@ -37,9 +56,39 @@ fun File.toExternalImage(): ExternalImage {
return ExternalImage(
width = image.getWidth(0),
height = image.getHeight(0),
md5 = this.md5(),
md5 = input.md5(),
imageFormat = image.formatName,
input = this.inputStream().asInput(IoBuffer.Pool),
inputSize = this.length()
)
}
/**
* 下载文件到临时目录然后调用 [File.toExternalImage]
*/
@Throws(IOException::class)
fun URL.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
openStream().transferTo(FileOutputStream(file))
return file.toExternalImage()
}
/**
* 保存为临时文件然后调用 [File.toExternalImage]
*/
@Throws(IOException::class)
fun InputStream.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
this.transferTo(FileOutputStream(file))
return file.toExternalImage()
}
/**
* 保存为临时文件然后调用 [File.toExternalImage]
*/
@Throws(IOException::class)
fun Input.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
this.asStream().transferTo(FileOutputStream(file))
return file.toExternalImage()
}
\ No newline at end of file
......@@ -10,7 +10,9 @@ import io.ktor.http.content.OutgoingContent
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.io.core.Input
import kotlinx.io.core.readFully
import java.io.File
import java.io.DataInput
import java.io.EOFException
import java.io.InputStream
import java.io.OutputStream
import java.net.InetAddress
import java.security.MessageDigest
......@@ -26,10 +28,10 @@ actual fun crc32(key: ByteArray): Int = CRC32().let { it.update(key); it.value.t
actual fun md5(byteArray: ByteArray): ByteArray = MessageDigest.getInstance("MD5").digest(byteArray)
fun File.md5(): ByteArray {
fun InputStream.md5(): ByteArray {
val digest = MessageDigest.getInstance("md5")
digest.reset()
this.inputStream().transferTo(object : OutputStream() {
this.transferTo(object : OutputStream() {
override fun write(b: Int) {
b.toByte().let {
digest.update(it)
......@@ -39,6 +41,21 @@ fun File.md5(): ByteArray {
return digest.digest()
}
fun DataInput.md5(): ByteArray {
val digest = MessageDigest.getInstance("md5")
digest.reset()
val buffer = byteArrayOf(1)
while (true) {
try {
this.readFully(buffer)
} catch (e: EOFException) {
break
}
digest.update(buffer[0])
}
return digest.digest()
}
actual fun solveIpAddress(hostname: String): String = InetAddress.getByName(hostname).hostAddress
actual fun localIpAddress(): String = InetAddress.getLocalHost().hostAddress
......
......@@ -6,6 +6,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.event.events.FriendMessageEvent
import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.subscribeAll
......
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