1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-25 21:23:55 +08:00

Add extensions for images

This commit is contained in:
Him188 2019-10-26 18:43:53 +08:00
parent 47ece90d60
commit 0a471e9b31
17 changed files with 327 additions and 75 deletions
mirai-core/src
mirai-demos/mirai-demo-1/src/main/java/demo1

View File

@ -1,5 +1,10 @@
package net.mamoe.mirai
/**
* Mirai 的一些信息.
*
* @see MiraiEnvironment 环境信息
*/
object Mirai {
const val VERSION: String = "1.0.0"
}

View File

@ -0,0 +1,3 @@
package net.mamoe.mirai
expect object MiraiEnvironment

View 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.

View File

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

View File

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

View File

@ -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 ====================================
/**

View File

@ -10,31 +10,44 @@ 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)
require(
httpPostFriendImage(
botAccount = bot.qqAccount,
uKeyHex = it.uKey!!.toUHexString(""),
imageInput = image.input,
inputSize = image.inputSize
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,
uKeyHex = it.uKey!!.toUHexString(""),
imageInput = image.input,
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
}
}
}

View File

@ -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
}
/**

View File

@ -0,0 +1,18 @@
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 } }
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
@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() }
}

View File

@ -0,0 +1,98 @@
@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

View File

@ -1,45 +0,0 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.utils
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.buildPacket
import kotlinx.io.streams.asInput
import java.io.File
import java.io.OutputStream
import java.security.MessageDigest
import javax.imageio.ImageIO
import java.awt.image.BufferedImage as JavaBufferedImage
fun JavaBufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
val digest = MessageDigest.getInstance("md5")
digest.reset()
val buffer = buildPacket {
ImageIO.write(this@toExternalImage, formatName, object : OutputStream() {
override fun write(b: Int) {
b.toByte().let {
this@buildPacket.writeByte(it)
digest.update(it)
}
}
})
}
return ExternalImage(width, height, digest.digest(), formatName, buffer)
}
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")
image.input = input
return ExternalImage(
width = image.getWidth(0),
height = image.getHeight(0),
md5 = this.md5(),
imageFormat = image.formatName,
input = this.inputStream().asInput(IoBuffer.Pool),
inputSize = this.length()
)
}

View File

@ -0,0 +1,94 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
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
/*
* 将各类型图片容器转为 [ExternalImage]
*/
/**
* 读取 [BufferedImage] 的属性, 然后构造 [ExternalImage]
*/
@Throws(IOException::class)
fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
val digest = MessageDigest.getInstance("md5")
digest.reset()
val buffer = buildPacket {
ImageIO.write(this@toExternalImage, formatName, object : OutputStream() {
override fun write(b: Int) {
b.toByte().let {
this@buildPacket.writeByte(it)
digest.update(it)
}
}
})
}
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")
image.input = input
return ExternalImage(
width = image.getWidth(0),
height = image.getHeight(0),
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()
}

View 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

View File

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