Graceful ImageUploading

This commit is contained in:
Him188 2019-10-27 03:37:00 +08:00
parent de86041d44
commit ff160c20c8
7 changed files with 203 additions and 33 deletions

View File

@ -4,15 +4,15 @@ package net.mamoe.mirai.message
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.QQ 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.network.protocol.tim.packet.action.FriendImageIdRequestPacket
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
// region Message Base
/** /**
* 可发送的或从服务器接收的消息. * 可发送的或从服务器接收的消息.
* 采用这样的消息模式是因为 QQ 的消息多元化, 一条消息中可包含 [纯文本][PlainText], [图片][Image] . * 采用这样的消息模式是因为 QQ 的消息多元化, 一条消息中可包含 [纯文本][PlainText], [图片][Image] .
* *
* #### Kotlin 使用 [Message] * ** Kotlin 使用 [Message]**
* 这与使用 [String] 的使用非常类似. * 这与使用 [String] 的使用非常类似.
* *
* 比较 [Message] [String] (使用 infix [Message.eq]): * 比较 [Message] [String] (使用 infix [Message.eq]):
@ -44,6 +44,15 @@ interface Message {
*/ */
val stringValue: String val stringValue: String
/**
* 类型 Key.
* [MessageChain] , 每个 [Message] 类型都拥有一个`伴生对象`(companion object) 来持有一个 Key
* [MessageChain.get] 时将会使用到这个 Key 进行判断类型.
*
* @param M 指代持有它的消息类型
*/
interface Key<M>
infix fun eq(other: Message): Boolean = this == other infix fun eq(other: Message): Boolean = this == other
/** /**
@ -76,20 +85,26 @@ interface Message {
infix operator fun plus(another: Message): MessageChain = this.concat(another) infix operator fun plus(another: Message): MessageChain = this.concat(another)
infix operator fun plus(another: String): MessageChain = this.concat(another.toMessage()) infix operator fun plus(another: String): MessageChain = this.concat(another.toMessage())
infix operator fun plus(another: Number): MessageChain = this.concat(another.toString().toMessage())
} }
/** /**
* [this] 发送给指定联系人 * [this] 发送给指定联系人
*/ */
suspend fun Message.sendTo(contact: Contact) = contact.sendMessage(this) suspend fun Message.sendTo(contact: Contact) = contact.sendMessage(this)
// endregion
// region PlainText
// ==================================== PlainText ==================================== // ==================================== PlainText ====================================
inline class PlainText(override val stringValue: String) : Message { inline class PlainText(override val stringValue: String) : Message {
override operator fun contains(sub: String): Boolean = sub in stringValue override operator fun contains(sub: String): Boolean = sub in stringValue
} override fun toString(): String = stringValue
companion object Key : Message.Key<PlainText>
}
// endregion
// region Image
// ==================================== Image ==================================== // ==================================== Image ====================================
/** /**
@ -100,6 +115,9 @@ inline class PlainText(override val stringValue: String) : Message {
*/ */
inline class Image(val id: ImageId) : Message { inline class Image(val id: ImageId) : Message {
override val stringValue: String get() = "[${id.value}]" override val stringValue: String get() = "[${id.value}]"
override fun toString(): String = stringValue
companion object Key : Message.Key<Image>
} }
/** /**
@ -116,6 +134,10 @@ fun ImageId.image(): Image = Image(this)
suspend fun ImageId.sendTo(contact: Contact) = contact.sendMessage(this.image()) suspend fun ImageId.sendTo(contact: Contact) = contact.sendMessage(this.image())
// endregion
// region At
// ==================================== At ==================================== // ==================================== At ====================================
/** /**
@ -125,8 +147,14 @@ inline class At(val targetQQ: UInt) : Message {
constructor(target: QQ) : this(target.id) constructor(target: QQ) : this(target.id)
override val stringValue: String get() = "[@$targetQQ]" override val stringValue: String get() = "[@$targetQQ]"
} override fun toString(): String = stringValue
companion object Key : Message.Key<At>
}
// endregion
// region Face
// ==================================== Face ==================================== // ==================================== Face ====================================
/** /**
@ -134,10 +162,17 @@ inline class At(val targetQQ: UInt) : Message {
*/ */
inline class Face(val id: FaceID) : Message { inline class Face(val id: FaceID) : Message {
override val stringValue: String get() = "[face${id.value}]" override val stringValue: String get() = "[face${id.value}]"
} override fun toString(): String = stringValue
companion object Key : Message.Key<Face>
}
// endregion Face
// region MessageChain
// ==================================== MessageChain ==================================== // ==================================== MessageChain ====================================
// region constructors
/** /**
* 构造无初始元素的可修改的 [MessageChain]. 初始大小将会被设定为 8 * 构造无初始元素的可修改的 [MessageChain]. 初始大小将会被设定为 8
*/ */
@ -178,6 +213,27 @@ fun SingleMessageChain(delegate: Message): MessageChain {
require(delegate !is MessageChain) { "delegate for SingleMessageChain should not be any instance of MessageChain" } require(delegate !is MessageChain) { "delegate for SingleMessageChain should not be any instance of MessageChain" }
return SingleMessageChainImpl(delegate) return SingleMessageChainImpl(delegate)
} }
// endregion
// region extensions
/**
* 获取第一个 [M] 类型的 [Message] 实例
*/
inline fun <reified M : Message> MessageChain.firstOrNull(): Message? = this.firstOrNull { M::class.isInstance(it) }
/**
* 获取第一个 [M] 类型的 [Message] 实例
* @throws [NoSuchElementException] 如果找不到该类型的实例
*/
inline fun <reified M : Message> MessageChain.first(): Message = this.first { M::class.isInstance(it) }
/**
* 获取第一个 [M] 类型的 [Message] 实例
*/
inline fun <reified M : Message> MessageChain.any(): Boolean = this.firstOrNull { M::class.isInstance(it) } !== null
// endregion
/** /**
* 消息链. MutableList<Message>. * 消息链. MutableList<Message>.
@ -204,6 +260,20 @@ interface MessageChain : Message, MutableList<Message> {
operator fun plusAssign(plain: String) { operator fun plusAssign(plain: String) {
this.concat(plain.toMessage()) this.concat(plain.toMessage())
} }
/**
* 获取第一个类型为 [key] [Message] 实例
*
* @param key 由各个类型消息的伴生对象持有. [PlainText.Key]
*/
@Suppress("UNCHECKED_CAST")
operator fun <M> get(key: Message.Key<M>): M = when (key) {
At -> first<At>()
PlainText -> first<PlainText>()
Image -> first<Image>()
Face -> first<Face>()
else -> error("unknown key: $key")
} as M
} }
/** /**
@ -403,15 +473,4 @@ internal inline class SingleMessageChainImpl(
override operator fun contains(element: Message): Boolean = element === delegate override operator fun contains(element: Message): Boolean = element === delegate
override val size: Int get() = 1 override val size: Int get() = 1
// endregion // endregion
} }
/**
* 获取第一个 [M] 类型的实例
*/
inline fun <reified M : Message> MessageChain.firstOrNull(): Message? = this.firstOrNull { M::class.isInstance(it) }
/**
* 获取第一个 [M] 类型的实例
* @throws [NoSuchElementException] 如果找不到该类型的实例
*/
inline fun <reified M : Message> MessageChain.first(): Message = this.first { M::class.isInstance(it) }

View File

@ -23,6 +23,9 @@ import net.mamoe.mirai.withSession
/** /**
* 上传图片 * 上传图片
* 挂起直到上传完成或失败 * 挂起直到上传完成或失败
*
* JVM , `SendImageUtilsJvm.kt` 内有多个捷径函数
*
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时 * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
*/ */
suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession { suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {

View File

@ -26,6 +26,9 @@ class OverFileSizeMaxException : IllegalStateException()
/** /**
* 上传群图片 * 上传群图片
* 挂起直到上传完成或失败 * 挂起直到上传完成或失败
*
* JVM , `SendImageUtilsJvm.kt` 内有多个捷径函数
*
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时 * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
*/ */
suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession { suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {

View File

@ -7,7 +7,9 @@ import kotlinx.io.core.Input
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.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.Image
import net.mamoe.mirai.message.ImageId import net.mamoe.mirai.message.ImageId
import net.mamoe.mirai.message.image
import net.mamoe.mirai.message.sendTo import net.mamoe.mirai.message.sendTo
import net.mamoe.mirai.network.protocol.tim.packet.action.uploadImage import net.mamoe.mirai.network.protocol.tim.packet.action.uploadImage
@ -22,7 +24,8 @@ fun ExternalImage(
/** /**
* 外部图片. 图片数据还没有读取到内存. * 外部图片. 图片数据还没有读取到内存.
* @see ExternalImage.sendTo * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人
* @See ExternalImage.upload 上传图片并得到 [Image] 消息
*/ */
class ExternalImage( class ExternalImage(
val width: Int, val width: Int,
@ -58,10 +61,21 @@ suspend fun ExternalImage.sendTo(contact: Contact) = when (contact) {
is QQ -> contact.uploadImage(this).sendTo(contact) is QQ -> contact.uploadImage(this).sendTo(contact)
} }
/**
* 上传图片并通过图片 ID 构造 [Image]
* 这个函数可能需消耗一段时间
*
* @see contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人
*/
suspend fun ExternalImage.upload(contact: Contact): Image = when (contact) {
is Group -> contact.uploadImage(this).image()
is QQ -> contact.uploadImage(this).image()
}
/** /**
* 将图片发送给 [this] * 将图片发送给 [this]
*/ */
suspend fun Contact.sendMessage(image: ExternalImage) = image.sendTo(this) suspend fun Contact.sendImage(image: ExternalImage) = image.sendTo(this)
private operator fun ByteArray.get(range: IntRange): String = buildString { private operator fun ByteArray.get(range: IntRange): String = buildString {
range.forEach { range.forEach {

View File

@ -0,0 +1,20 @@
package net.mamoe.mirai.utils.internal
@PublishedApi
internal fun Int.coerceAtLeastOrFail(value: Int): Int {
require(this > value)
return this
}
@PublishedApi
internal fun Long.coerceAtLeastOrFail(value: Long): Long {
require(this > value)
return this
}
/**
* 表示这个参数必须为正数
*/
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.VALUE_PARAMETER)
internal annotation class PositiveNumbers

View File

@ -2,13 +2,6 @@
package net.mamoe.mirai package net.mamoe.mirai
import java.io.File
actual typealias MiraiEnvironment = MiraiEnvironmentJvm actual typealias MiraiEnvironment = MiraiEnvironmentJvm
object MiraiEnvironmentJvm { object MiraiEnvironmentJvm
/**
* JVM only, 临时文件夹
*/
val TEMP_DIR: File = createTempDir().apply { deleteOnExit() }
}

View File

@ -9,6 +9,7 @@ import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.network.protocol.tim.packet.action.OverFileSizeMaxException import net.mamoe.mirai.network.protocol.tim.packet.action.OverFileSizeMaxException
import net.mamoe.mirai.utils.sendTo import net.mamoe.mirai.utils.sendTo
import net.mamoe.mirai.utils.toExternalImage import net.mamoe.mirai.utils.toExternalImage
import net.mamoe.mirai.utils.upload
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -18,14 +19,14 @@ import java.net.URL
* 发送图片的一些扩展函数. * 发送图片的一些扩展函数.
*/ */
// region Type extensions // region IMAGE.sendAsImageTo(Contact)
/** /**
* 将图片发送到指定联系人. 不会保存临时文件 * 将图片发送到指定联系人. 不会创建临时文件
* @throws OverFileSizeMaxException * @throws OverFileSizeMaxException
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun BufferedImage.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) suspend fun BufferedImage.sendTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/** /**
* 下载 [URL] 到临时文件并将其作为图片发送到指定联系人 * 下载 [URL] 到临时文件并将其作为图片发送到指定联系人
@ -57,15 +58,53 @@ suspend fun File.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) {
// endregion // endregion
// region IMAGE.Upload(Contact): Image
// region Contact extensions /**
* 将图片上传后构造 [Image]. 不会创建临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun BufferedImage.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* 下载 [URL] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun URL.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* 读取 [Input] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Input.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun InputStream.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* 将文件作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun File.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
// endregion
// region Contact.sendImage(IMAGE)
/** /**
* 将图片发送到指定联系人. 不会保存临时文件 * 将图片发送到指定联系人. 不会保存临时文件
* @throws OverFileSizeMaxException * @throws OverFileSizeMaxException
*/ */
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(bufferedImage: BufferedImage) = bufferedImage.sendAsImageTo(this) suspend fun Contact.sendImage(bufferedImage: BufferedImage) = bufferedImage.sendTo(this)
/** /**
* 下载 [URL] 到临时文件并将其作为图片发送到指定联系人 * 下载 [URL] 到临时文件并将其作为图片发送到指定联系人
@ -95,4 +134,43 @@ suspend fun Contact.sendImage(imageStream: InputStream) = imageStream.sendAsImag
@Throws(OverFileSizeMaxException::class) @Throws(OverFileSizeMaxException::class)
suspend fun Contact.sendImage(file: File) = file.sendAsImageTo(this) suspend fun Contact.sendImage(file: File) = file.sendAsImageTo(this)
// endregion
// region Contact.uploadImage(IMAGE)
/**
* 将图片发送到指定联系人. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.uploadImage(bufferedImage: BufferedImage): Image = bufferedImage.upload(this)
/**
* 下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.uploadImage(imageUrl: URL): Image = imageUrl.upload(this)
/**
* 读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.uploadImage(imageInput: Input): Image = imageInput.upload(this)
/**
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.uploadImage(imageStream: InputStream): Image = imageStream.upload(this)
/**
* 将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Contact.uploadImage(file: File): Image = file.upload(this)
// endregion // endregion