Simplify platform structure: merge jvmMain into commonMain

This commit is contained in:
Him188 2020-12-01 12:13:43 +08:00
parent c030155505
commit 66464b87fd
13 changed files with 289 additions and 903 deletions

View File

@ -22,7 +22,6 @@ import net.mamoe.mirai.message.recall
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.WeakRefProperty
import kotlin.jvm.JvmSynthetic
/**

View File

@ -14,7 +14,6 @@
package net.mamoe.mirai.event
import kotlinx.coroutines.*
import net.mamoe.mirai.event.internal.registerEvent
import java.lang.reflect.Method
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

View File

@ -15,11 +15,12 @@
@file:JvmMultifileClass
@file:JvmName("SendImageUtilsJvmKt")
package net.mamoe.mirai.message
package event
import kotlinx.coroutines.Dispatchers
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Voice
import net.mamoe.mirai.utils.*

View File

@ -18,6 +18,7 @@
package net.mamoe.mirai.message
import event.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.events.BotEvent
@ -26,9 +27,9 @@ import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.sendTo
import net.mamoe.mirai.utils.upload
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
/**
* 一个 (收到的) 消息事件.
@ -158,9 +159,60 @@ public interface MessageEventExtensions<out TSender : User, out TSubject : Conta
}
/** 一个消息事件在各平台的相关扩展. 请使用 [MessageEventExtensions] */
internal expect interface MessageEventPlatformExtensions<out TSender : User, out TSubject : Contact> {
/**
* 消息事件在 JVM 平台的扩展
* @see MessageEventExtensions
*/
internal interface MessageEventPlatformExtensions<out TSender : User, out TSubject : Contact> {
val subject: TSubject
val sender: TSender
val message: MessageChain
val bot: Bot
}
// region 上传图片
@JvmSynthetic
suspend fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)
@JvmSynthetic
suspend fun uploadImage(image: InputStream): Image = subject.uploadImage(image)
@JvmSynthetic
suspend fun uploadImage(image: File): Image = subject.uploadImage(image)
// endregion
// region 发送图片
@JvmSynthetic
suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject> = subject.sendImage(image)
@JvmSynthetic
suspend fun sendImage(image: InputStream): MessageReceipt<TSubject> = subject.sendImage(image)
@JvmSynthetic
suspend fun sendImage(image: File): MessageReceipt<TSubject> = subject.sendImage(image)
// endregion
// region 上传图片 (扩展)
@JvmSynthetic
suspend fun BufferedImage.upload(): Image = upload(subject)
@JvmSynthetic
suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
@JvmSynthetic
suspend fun File.uploadAsImage(): Image = uploadAsImage(subject)
// endregion 上传图片 (扩展)
// region 发送图片 (扩展)
@JvmSynthetic
suspend fun BufferedImage.send(): MessageReceipt<TSubject> = sendTo(subject)
@JvmSynthetic
suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
@JvmSynthetic
suspend fun File.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
// endregion 发送图片 (扩展)
}

View File

@ -22,18 +22,18 @@
package net.mamoe.mirai.message.data
import kotlinx.io.core.Input
import net.mamoe.mirai.Bot
import net.mamoe.mirai.IMirai
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.sendImage
import java.io.File
import java.io.InputStream
import java.net.URL
import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* 自定义表情 (收藏的表情) 和普通图片.
@ -43,18 +43,24 @@ import kotlin.jvm.JvmSynthetic
* 在上传时服务器会根据其缓存情况回复已有的图片 ID 或要求客户端上传. 详见 [Contact.uploadImage]
*
*
* ### [toString] [contentToString]
* - [toString] 固定返回 `[mirai:image:<ID>]` 格式字符串, 其中 `<ID>` 代表 [imageId].
* - [contentToString] 固定返回 "\[图片]"
*
* ### 上传和发送图片
* @see Contact.uploadImage 上传 [图片文件][ExternalImage] 并得到 [Image] 消息
* @see Contact.sendImage 上传 [图片文件][ExternalImage] 并发送返回的 [Image] 作为一条消息
* @see Image.sendTo 上传图片并得到 [Image] 消息
* @see Image.sendTo 上传 [图片文件][ExternalImage] 并得到 [Image] 消息
*
* @see File.uploadAsImage
* @see InputStream.uploadAsImage
* @see Input.uploadAsImage
* @see URL.uploadAsImage
*
* @see File.sendAsImageTo
* @see InputStream.sendAsImageTo
* @see Input.sendAsImageTo
* @see URL.sendAsImageTo
*
* ### 下载图片
* @see Image.queryUrl 扩展函数. 查询图片下载链接
* @see IMirai.queryImageUrl 查询图片下载链接 (Java 使用)
* @see Bot.queryImageUrl 查询图片下载链接 (Java 使用)
*
* 查看平台 `actual` 定义以获取上传方式扩展.
*
@ -64,11 +70,12 @@ import kotlin.jvm.JvmSynthetic
* @see FlashImage 闪照
* @see Image.flash 转换普通图片为闪照
*/
public expect interface Image : Message, MessageContent, CodableMessage {
public interface Image : Message, MessageContent, CodableMessage {
public companion object Key : Message.Key<Image> {
public override val typeName: String
override val typeName: String get() = "Image"
}
/**
* 图片的 id.
*
@ -76,24 +83,16 @@ public expect interface Image : Message, MessageContent, CodableMessage {
*
* ### 格式
* 群图片:
* - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext` (ext系扩展名)
* - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 ".mirai")
*
* 好友图片:
* - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
* - [FRIEND_IMAGE_ID_REGEX_2], 示例: `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206`
*
* @see Image 使用 id 构造图片
* @see md5 得到图片文件 MD5
*/
public val imageId: String
/* 实现:
final override fun toString(): String = _stringValue!!
final override fun contentToString(): String = "[图片]"
*/
}
/**
* 所有 [Image] 实现的基类.
*/

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.internal.DeferredReusableInput
import net.mamoe.mirai.utils.internal.asReusableInput
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
/*
* 将各类型图片容器转为 [ExternalImage]
*/
/**
* [BufferedImage] 保存为临时文件, 然后构造 [ExternalImage]
*/
@JvmOverloads
public fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage =
ExternalImage(DeferredReusableInput(this, formatName))
/**
* 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据.
* @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件
*/
@JvmOverloads
public fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage {
require(this.isFile) { "File must be a file" }
require(this.exists()) { "File must exist" }
require(this.canRead()) { "File must can be read" }
return ExternalImage(asReusableInput(deleteOnClose))
}
/**
* [InputStream] 委托为 [ExternalImage].
* 只会在上传图片时才读取 [InputStream] 的内容. 具体行为取决于相关 [Bot] [FileCacheStrategy]
*/
public fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
/**
* [ByteArray] 委托为 [ExternalImage].
*/
public fun ByteArray.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))

View File

@ -9,10 +9,17 @@
package net.mamoe.mirai.utils
import kotlinx.io.core.Closeable
import kotlinx.io.core.Input
import kotlinx.io.core.use
import kotlinx.io.errors.IOException
import kotlinx.io.core.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.internal.asReusableInput
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import java.security.MessageDigest
import javax.imageio.ImageIO
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@ -20,43 +27,179 @@ import kotlin.contracts.contract
* 缓存策略.
*
* 图片上传时默认使用文件缓存.
*
* @see BotConfiguration.fileCacheStrategy [Bot] 指定缓存策略
*/
@MiraiExperimentalApi
public expect interface FileCacheStrategy {
public interface FileCacheStrategy {
/**
* [input] 缓存为 [ExternalImage].
* 此函数应 close 这个 [Input]
*/
@MiraiExperimentalApi
@Throws(IOException::class)
@Throws(java.io.IOException::class)
public fun newImageCache(input: Input): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
* 此函数应 close 这个 [InputStream]
*/
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
public fun newImageCache(input: InputStream): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
* [input] 的内容应是不变的.
*/
@MiraiExperimentalApi
@Throws(IOException::class)
@Throws(java.io.IOException::class)
public fun newImageCache(input: ByteArray): ExternalImage
/**
* 默认的缓存方案. JVM 平台使用系统临时文件.
* [input] 缓存为 [ExternalImage].
* [input] 的内容应是不变的.
*/
@MiraiExperimentalApi
public object PlatformDefault : FileCacheStrategy
@Throws(java.io.IOException::class)
public fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
*/
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
public fun newImageCache(input: URL): ExternalImage
/**
* 默认的缓存方案, 使用系统临时文件夹存储.
*/
@MiraiExperimentalApi
public object PlatformDefault : FileCacheStrategy by TempCache(null)
/**
* 使用内存直接存储所有图片文件.
*/
public object MemoryCache : FileCacheStrategy {
@MiraiExperimentalApi
@Throws(IOException::class)
public override fun newImageCache(input: Input): ExternalImage
@Throws(java.io.IOException::class)
override fun newImageCache(input: Input): ExternalImage {
return newImageCache(input.readBytes())
}
@MiraiExperimentalApi
@Throws(IOException::class)
public override fun newImageCache(input: ByteArray): ExternalImage
@Throws(java.io.IOException::class)
override fun newImageCache(input: InputStream): ExternalImage {
return newImageCache(input.readBytes())
}
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
override fun newImageCache(input: ByteArray): ExternalImage {
return ExternalImage(input.asReusableInput())
}
@MiraiExperimentalApi
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
val out = ByteArrayOutputStream()
ImageIO.write(input, format, out)
return newImageCache(out.toByteArray())
}
@MiraiExperimentalApi
override fun newImageCache(input: URL): ExternalImage {
val out = ByteArrayOutputStream()
input.openConnection().getInputStream().use { it.copyTo(out) }
return newImageCache(out.toByteArray())
}
}
/**
* 使用系统临时文件夹缓存图片文件. 在图片使用完毕后删除临时文件.
*/
@MiraiExperimentalApi
public class TempCache @JvmOverloads constructor(
/**
* 缓存图片存放位置. `null` 时使用主机系统的临时文件夹
*/
public val directory: File? = null
) : FileCacheStrategy {
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
override fun newImageCache(input: Input): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
override fun newImageCache(input: InputStream): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
@MiraiExperimentalApi
@Throws(java.io.IOException::class)
override fun newImageCache(input: ByteArray): ExternalImage {
return ExternalImage(input.asReusableInput())
}
@MiraiExperimentalApi
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
val file = createTempFile(directory = directory).apply { deleteOnExit() }
val digest = MessageDigest.getInstance("md5")
digest.reset()
file.outputStream().use { out ->
ImageIO.write(input, format, object : OutputStream() {
override fun write(b: Int) {
out.write(b)
digest.update(b.toByte())
}
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)
}
})
}
@Suppress("DEPRECATION_ERROR")
return ExternalImage(file.asReusableInput(true, digest.digest()))
}
@MiraiExperimentalApi
override fun newImageCache(input: URL): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
}
}
@Throws(java.io.IOException::class)
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = readAvailable(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = readAvailable(buffer)
}
return bytesCopied
}
internal inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {

View File

@ -1,193 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.event.internal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.event.*
import java.lang.reflect.Method
import java.util.function.Consumer
import java.util.function.Function
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.reflect.KClass
import kotlin.reflect.full.IllegalCallableAccessException
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.kotlinFunction
@Suppress("FunctionName")
internal fun <E : Event> Class<E>._subscribeEventForJaptOnly(
scope: CoroutineScope,
onEvent: Function<E, ListeningStatus>
): Listener<E> {
return this.kotlin.subscribeInternal(
scope.Handler(
scope.coroutineContext,
Listener.ConcurrencyKind.LOCKED
) { withContext(Dispatchers.IO) { onEvent.apply(it) } })
}
@Suppress("FunctionName")
internal fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer<E>): Listener<E> {
return this.kotlin.subscribeInternal(
scope.Handler(
EmptyCoroutineContext,
Listener.ConcurrencyKind.LOCKED
) { withContext(Dispatchers.IO) { onEvent.accept(it) }; ListeningStatus.LISTENING; })
}
@Suppress("UNCHECKED_CAST")
internal fun Method.registerEvent(
owner: Any,
scope: CoroutineScope,
annotation: EventHandler,
coroutineContext: CoroutineContext
): Listener<Event> {
this.isAccessible = true
val kotlinFunction = kotlin.runCatching { this.kotlinFunction }.getOrNull()
return if (kotlinFunction != null) {
// kotlin functions
val param = kotlinFunction.parameters
when (param.size) {
3 -> { // ownerClass, receiver, event
check(param[1].type == param[2].type) { "Illegal kotlin function ${kotlinFunction.name}. Receiver and param must have same type" }
check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) {
"Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}"
}
}
2 -> { // ownerClass, event
check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) {
"Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}"
}
}
else -> error("function ${kotlinFunction.name} must have one Event param")
}
lateinit var listener: Listener<*>
kotlin.runCatching {
kotlinFunction.isAccessible = true
}
suspend fun callFunction(event: Event): Any? {
try {
return when (param.size) {
3 -> {
if (kotlinFunction.isSuspend) {
kotlinFunction.callSuspend(owner, event, event)
} else withContext(Dispatchers.IO) { // for safety
kotlinFunction.call(owner, event, event)
}
}
2 -> {
if (kotlinFunction.isSuspend) {
kotlinFunction.callSuspend(owner, event)
} else withContext(Dispatchers.IO) { // for safety
kotlinFunction.call(owner, event)
}
}
else -> error("stub")
}
} catch (e: IllegalCallableAccessException) {
listener.completeExceptionally(e)
return ListeningStatus.STOPPED
}
}
require(!kotlinFunction.returnType.isMarkedNullable) {
"Kotlin event handlers cannot have nullable return type."
}
require(kotlinFunction.parameters.none { it.type.isMarkedNullable }) {
"Kotlin event handlers cannot have nullable parameter type."
}
when (kotlinFunction.returnType.classifier) {
Unit::class, Nothing::class -> {
scope.subscribeAlways(
param[1].type.classifier as KClass<out Event>,
priority = annotation.priority,
concurrency = annotation.concurrency,
coroutineContext = coroutineContext
) {
if (annotation.ignoreCancelled) {
if ((this as? CancellableEvent)?.isCancelled != true) {
callFunction(this)
}
} else callFunction(this)
}.also { listener = it }
}
ListeningStatus::class -> {
scope.subscribe(
param[1].type.classifier as KClass<out Event>,
priority = annotation.priority,
concurrency = annotation.concurrency,
coroutineContext = coroutineContext
) {
if (annotation.ignoreCancelled) {
if ((this as? CancellableEvent)?.isCancelled != true) {
callFunction(this) as ListeningStatus
} else ListeningStatus.LISTENING
} else callFunction(this) as ListeningStatus
}.also { listener = it }
}
else -> error("Illegal method return type. Required Void, Nothing or ListeningStatus, found ${kotlinFunction.returnType.classifier}")
}
} else {
// java methods
val paramType = this.parameters[0].type
check(this.parameterCount == 1 && Event::class.java.isAssignableFrom(paramType)) {
"Illegal method parameter. Required one exact Event subclass. found $paramType"
}
when (this.returnType) {
Void::class.java, Void.TYPE, Nothing::class.java -> {
scope.subscribeAlways(
paramType.kotlin as KClass<out Event>,
priority = annotation.priority,
concurrency = annotation.concurrency,
coroutineContext = coroutineContext
) {
if (annotation.ignoreCancelled) {
if ((this as? CancellableEvent)?.isCancelled != true) {
withContext(Dispatchers.IO) {
this@registerEvent.invoke(owner, this@subscribeAlways)
}
}
} else withContext(Dispatchers.IO) {
this@registerEvent.invoke(owner, this@subscribeAlways)
}
}
}
ListeningStatus::class.java -> {
scope.subscribe(
paramType.kotlin as KClass<out Event>,
priority = annotation.priority,
concurrency = annotation.concurrency,
coroutineContext = coroutineContext
) {
if (annotation.ignoreCancelled) {
if ((this as? CancellableEvent)?.isCancelled != true) {
withContext(Dispatchers.IO) {
this@registerEvent.invoke(owner, this@subscribe) as ListeningStatus
}
} else ListeningStatus.LISTENING
} else withContext(Dispatchers.IO) {
this@registerEvent.invoke(owner, this@subscribe) as ListeningStatus
}
}
}
else -> error("Illegal method return type. Required Void or ListeningStatus, but found ${this.returnType.canonicalName}")
}
}
}

View File

@ -1,149 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.message
import kotlinx.io.core.Input
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
import java.net.URL
/**
* 消息事件在 JVM 平台的扩展
* @see MessageEventExtensions
*/
internal actual interface MessageEventPlatformExtensions<out TSender : User, out TSubject : Contact> {
actual val subject: TSubject
actual val sender: TSender
actual val message: MessageChain
actual val bot: Bot
// region 上传图片
@JvmSynthetic
suspend fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)
@JvmSynthetic
suspend fun uploadImage(image: InputStream): Image = subject.uploadImage(image)
@JvmSynthetic
suspend fun uploadImage(image: File): Image = subject.uploadImage(image)
// endregion
// region 发送图片
@JvmSynthetic
suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject> = subject.sendImage(image)
@JvmSynthetic
suspend fun sendImage(image: InputStream): MessageReceipt<TSubject> = subject.sendImage(image)
@JvmSynthetic
suspend fun sendImage(image: File): MessageReceipt<TSubject> = subject.sendImage(image)
// endregion
// region 上传图片 (扩展)
@JvmSynthetic
suspend fun BufferedImage.upload(): Image = upload(subject)
@JvmSynthetic
suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
@JvmSynthetic
suspend fun File.uploadAsImage(): Image = uploadAsImage(subject)
// endregion 上传图片 (扩展)
// region 发送图片 (扩展)
@JvmSynthetic
suspend fun BufferedImage.send(): MessageReceipt<TSubject> = sendTo(subject)
@JvmSynthetic
suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
@JvmSynthetic
suspend fun File.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
// endregion 发送图片 (扩展)
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@JvmSynthetic
@Suppress("DEPRECATION")
suspend fun URL.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
@JvmSynthetic
suspend fun Input.sendAsImage(): MessageReceipt<TSubject> = sendAsImageTo(subject)
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@JvmSynthetic
@Suppress("DEPRECATION")
suspend fun uploadImage(image: URL): Image = subject.uploadImage(image)
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
@JvmSynthetic
suspend fun uploadImage(image: Input): Image = subject.uploadImage(image)
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Suppress("DEPRECATION")
@JvmSynthetic
suspend fun sendImage(image: URL): MessageReceipt<TSubject> = subject.sendImage(image)
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
@JvmSynthetic
suspend fun sendImage(image: Input): MessageReceipt<TSubject> = subject.sendImage(image)
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Suppress("DEPRECATION")
@JvmSynthetic
suspend fun URL.uploadAsImage(): Image = uploadAsImage(subject)
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
@JvmSynthetic
suspend fun Input.uploadAsImage(): Image = uploadAsImage(subject)
}

View File

@ -1,86 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmMultifileClass
@file:JvmName("MessageUtils")
package net.mamoe.mirai.message.data
import kotlinx.io.core.Input
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.sendAsImageTo
import net.mamoe.mirai.message.sendImage
import net.mamoe.mirai.message.uploadAsImage
import net.mamoe.mirai.message.uploadImage
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.sendImage
import java.io.File
import java.io.InputStream
import java.net.URL
/**
* 自定义表情 (收藏的表情) 和普通图片.
*
*
* 最推荐的存储方式是存储图片原文件, 每次发送图片时都使用文件上传.
* 在上传时服务器会根据其缓存情况回复已有的图片 ID 或要求客户端上传. 详见 [Contact.uploadImage]
*
*
* ### 上传和发送图片
* @see Contact.uploadImage 上传 [图片文件][ExternalImage] 并得到 [Image] 消息
* @see Contact.sendImage 上传 [图片文件][ExternalImage] 并发送返回的 [Image] 作为一条消息
* @see Image.sendTo 上传 [图片文件][ExternalImage] 并得到 [Image] 消息
*
* @see File.uploadAsImage
* @see InputStream.uploadAsImage
* @see Input.uploadAsImage
* @see URL.uploadAsImage
*
* @see File.sendAsImageTo
* @see InputStream.sendAsImageTo
* @see Input.sendAsImageTo
* @see URL.sendAsImageTo
*
* ### 下载图片
* @see Image.queryUrl 扩展函数. 查询图片下载链接
* @see Bot.queryImageUrl 查询图片下载链接 (Java 使用)
*
* 查看平台 `actual` 定义以获取上传方式扩展.
*
* ## mirai 码支持
* 格式: &#91;mirai:image:*[Image.imageId]*&#93;
*
* @see FlashImage 闪照
* @see Image.flash 转换普通图片为闪照
*/
public actual interface Image : Message, MessageContent, CodableMessage {
public actual companion object Key : Message.Key<Image> {
actual override val typeName: String get() = "Image"
}
/**
* 图片的 id.
*
* 图片 id 不一定会长时间保存, 也可能在将来改变格式, 因此不建议使用 id 发送图片.
*
* ### 格式
* 群图片:
* - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 ".mirai")
*
* 好友图片:
* - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
* - [FRIEND_IMAGE_ID_REGEX_2], 示例: `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206`
*
* @see Image 使用 id 构造图片
*/
public actual val imageId: String
}

View File

@ -1,127 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("DEPRECATION", "DEPRECATION_ERROR")
@file:JvmMultifileClass
@file:JvmName("SendImageUtilsJvmKt")
package net.mamoe.mirai.message
import kotlinx.coroutines.Dispatchers
import kotlinx.io.core.Input
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.sendTo
import net.mamoe.mirai.utils.toExternalImage
import net.mamoe.mirai.utils.upload
import java.net.URL
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Throws(OverFileSizeMaxException::class)
public suspend fun <C : Contact> URL.sendAsImageTo(contact: C): MessageReceipt<C> =
toExternalImage().sendTo(contact)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
@Throws(OverFileSizeMaxException::class)
public suspend fun <C : Contact> Input.sendAsImageTo(contact: C): MessageReceipt<C> =
toExternalImage().sendTo(contact)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Throws(OverFileSizeMaxException::class)
public suspend fun URL.uploadAsImage(contact: Contact): Image =
toExternalImage().upload(contact)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Throws(OverFileSizeMaxException::class)
public suspend inline fun Contact.uploadImage(imageUrl: URL): Image = imageUrl.uploadAsImage(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Throws(OverFileSizeMaxException::class)
public suspend inline fun Contact.uploadImage(imageInput: Input): Image = imageInput.uploadAsImage(this)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().sendAsImageTo(contact)"),
level = DeprecationLevel.WARNING
)
@Throws(OverFileSizeMaxException::class)
public suspend inline fun <C : Contact> C.sendImage(imageUrl: URL): MessageReceipt<C> = imageUrl.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Throws(OverFileSizeMaxException::class)
public suspend inline fun <C : Contact> C.sendImage(imageInput: Input): MessageReceipt<C> =
imageInput.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
@Throws(OverFileSizeMaxException::class)
public suspend fun Input.uploadAsImage(contact: Contact): Image =
toExternalImage().upload(contact)

View File

@ -1,104 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.utils
import kotlinx.io.core.Input
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.internal.DeferredReusableInput
import net.mamoe.mirai.utils.internal.asReusableInput
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
import java.net.URL
/*
* 将各类型图片容器转为 [ExternalImage]
*/
/**
* [BufferedImage] 保存为临时文件, 然后构造 [ExternalImage]
*/
@JvmOverloads
public fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage =
ExternalImage(DeferredReusableInput(this, formatName))
/**
* 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据.
* @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件
*/
@JvmOverloads
public fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage {
require(this.isFile) { "File must be a file" }
require(this.exists()) { "File must exist" }
require(this.canRead()) { "File must can be read" }
return ExternalImage(asReusableInput(deleteOnClose))
}
/**
* [InputStream] 委托为 [ExternalImage].
* 只会在上传图片时才读取 [InputStream] 的内容. 具体行为取决于相关 [Bot] [FileCacheStrategy]
*/
public fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
/**
* [ByteArray] 委托为 [ExternalImage].
*/
public fun ByteArray.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
/**
* [URL] 委托为 [ExternalImage].
*
* 只会在上传图片时才读取 [URL] 的内容. 具体行为取决于相关 [Bot] [FileCacheStrategy]
*/
@Deprecated(
"请自行通过 URL.openConnection 得到 InputStream 后调用其扩展",
replaceWith = ReplaceWith("this.openConnection().toExternalImage"),
level = DeprecationLevel.WARNING
)
public fun URL.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
/**
* [Input] 委托为 [ExternalImage].
* 只会在上传图片时才读取 [Input] 的内容. 具体行为取决于相关 [Bot] [FileCacheStrategy]
*/
@Deprecated(
"已弃用对 kotlinx.io 的支持",
level = DeprecationLevel.ERROR
)
public fun Input.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null))
@PlannedRemoval("1.2.0")
@Suppress("RedundantSuspendModifier", "DEPRECATION_ERROR")
@Deprecated("no need", ReplaceWith("toExternalImage()"), level = DeprecationLevel.HIDDEN)
public suspend fun Input.suspendToExternalImage(): ExternalImage = toExternalImage()
@Suppress("RedundantSuspendModifier")
@PlannedRemoval("1.2.0")
@Deprecated("no need", ReplaceWith("toExternalImage()"), level = DeprecationLevel.HIDDEN)
public suspend fun InputStream.suspendToExternalImage(): ExternalImage = toExternalImage()
@Suppress("RedundantSuspendModifier", "DEPRECATION")
@PlannedRemoval("1.2.0")
@Deprecated("no need", ReplaceWith("toExternalImage()"), level = DeprecationLevel.HIDDEN)
public suspend fun URL.suspendToExternalImage(): ExternalImage = toExternalImage()
@Suppress("RedundantSuspendModifier")
@PlannedRemoval("1.2.0")
@Deprecated("no need", ReplaceWith("toExternalImage()"), level = DeprecationLevel.HIDDEN)
public suspend fun File.suspendToExternalImage(): ExternalImage = toExternalImage()
@Suppress("RedundantSuspendModifier")
@PlannedRemoval("1.2.0")
@Deprecated("no need", ReplaceWith("toExternalImage()"), level = DeprecationLevel.HIDDEN)
public suspend fun BufferedImage.suspendToExternalImage(): ExternalImage = toExternalImage()

View File

@ -1,202 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.utils
import kotlinx.io.core.Input
import kotlinx.io.core.readAvailable
import kotlinx.io.core.readBytes
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.internal.asReusableInput
import java.awt.image.BufferedImage
import java.io.*
import java.net.URL
import java.security.MessageDigest
import javax.imageio.ImageIO
/**
* 缓存策略.
*
* 图片上传时默认使用文件缓存.
*
* @see BotConfiguration.fileCacheStrategy [Bot] 指定缓存策略
*/
@MiraiExperimentalApi
public actual interface FileCacheStrategy {
/**
* [input] 缓存为 [ExternalImage].
* 此函数应 close 这个 [Input]
*/
@MiraiExperimentalApi
@Throws(IOException::class)
public actual fun newImageCache(input: Input): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
* 此函数应 close 这个 [InputStream]
*/
@MiraiExperimentalApi
@Throws(IOException::class)
public fun newImageCache(input: InputStream): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
* [input] 的内容应是不变的.
*/
@MiraiExperimentalApi
@Throws(IOException::class)
public actual fun newImageCache(input: ByteArray): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
* [input] 的内容应是不变的.
*/
@MiraiExperimentalApi
@Throws(IOException::class)
public fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage
/**
* [input] 缓存为 [ExternalImage].
*/
@MiraiExperimentalApi
@Throws(IOException::class)
public fun newImageCache(input: URL): ExternalImage
/**
* 默认的缓存方案, 使用系统临时文件夹存储.
*/
@MiraiExperimentalApi
public actual object PlatformDefault : FileCacheStrategy by TempCache(null)
/**
* 使用内存直接存储所有图片文件.
*/
public actual object MemoryCache : FileCacheStrategy {
@MiraiExperimentalApi
@Throws(IOException::class)
actual override fun newImageCache(input: Input): ExternalImage {
return newImageCache(input.readBytes())
}
@MiraiExperimentalApi
@Throws(IOException::class)
override fun newImageCache(input: InputStream): ExternalImage {
return newImageCache(input.readBytes())
}
@MiraiExperimentalApi
@Throws(IOException::class)
actual override fun newImageCache(input: ByteArray): ExternalImage {
return ExternalImage(input.asReusableInput())
}
@MiraiExperimentalApi
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
val out = ByteArrayOutputStream()
ImageIO.write(input, format, out)
return newImageCache(out.toByteArray())
}
@MiraiExperimentalApi
override fun newImageCache(input: URL): ExternalImage {
val out = ByteArrayOutputStream()
input.openConnection().getInputStream().use { it.copyTo(out) }
return newImageCache(out.toByteArray())
}
}
/**
* 使用系统临时文件夹缓存图片文件. 在图片使用完毕后删除临时文件.
*/
@MiraiExperimentalApi
public class TempCache @JvmOverloads constructor(
/**
* 缓存图片存放位置. `null` 时使用主机系统的临时文件夹
*/
public val directory: File? = null
) : FileCacheStrategy {
@MiraiExperimentalApi
@Throws(IOException::class)
override fun newImageCache(input: Input): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
@MiraiExperimentalApi
@Throws(IOException::class)
override fun newImageCache(input: InputStream): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
@MiraiExperimentalApi
@Throws(IOException::class)
override fun newImageCache(input: ByteArray): ExternalImage {
return ExternalImage(input.asReusableInput())
}
@MiraiExperimentalApi
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
val file = createTempFile(directory = directory).apply { deleteOnExit() }
val digest = MessageDigest.getInstance("md5")
digest.reset()
file.outputStream().use { out ->
ImageIO.write(input, format, object : OutputStream() {
override fun write(b: Int) {
out.write(b)
digest.update(b.toByte())
}
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)
}
})
}
@Suppress("DEPRECATION_ERROR")
return ExternalImage(file.asReusableInput(true, digest.digest()))
}
@MiraiExperimentalApi
override fun newImageCache(input: URL): ExternalImage {
return ExternalImage(createTempFile(directory = directory).apply {
deleteOnExit()
input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) }
}.asReusableInput(true))
}
}
}
@Throws(IOException::class)
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = readAvailable(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = readAvailable(buffer)
}
return bytesCopied
}