diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt index 0b4fd6ca8..86f7f7426 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/FriendImpl.kt @@ -87,6 +87,10 @@ internal class FriendImpl( @JvmSynthetic @OptIn(MiraiInternalAPI::class, ExperimentalStdlibApi::class, ExperimentalTime::class) override suspend fun uploadImage(image: ExternalImage): Image = try { + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { + image.input.init(bot.configuration.fileCacheStrategy) + } if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) { throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") } @@ -96,10 +100,10 @@ internal class FriendImpl( srcUin = bot.id.toInt(), dstUin = id.toInt(), fileId = 0, - fileMd5 = image.md5, + fileMd5 = @Suppress("INVISIBLE_MEMBER") image.md5, fileSize = @Suppress("INVISIBLE_MEMBER") image.input.size.toInt(), - fileName = image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName, + fileName = @Suppress("INVISIBLE_MEMBER") image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName, imgOriginal = 1 ) ).sendAndExpect() diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt index 912985d75..3de8f7418 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt @@ -406,6 +406,10 @@ internal class GroupImpl( @OptIn(ExperimentalTime::class) @JvmSynthetic override suspend fun uploadImage(image: ExternalImage): OfflineGroupImage = try { + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { + image.input.init(bot.configuration.fileCacheStrategy) + } if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) { throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt index 5cc703564..6db8c41af 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + package net.mamoe.mirai.qqandroid.network.highway import io.ktor.client.HttpClient @@ -34,9 +36,9 @@ import net.mamoe.mirai.qqandroid.utils.addSuppressedMirai import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf import net.mamoe.mirai.qqandroid.utils.io.withUse import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString -import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.internal.ReusableInput import net.mamoe.mirai.utils.verbose import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.roundToInt @@ -44,12 +46,12 @@ import kotlin.time.ExperimentalTime import kotlin.time.measureTime @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) -@Suppress("SpellCheckingInspection", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@Suppress("SpellCheckingInspection") internal suspend fun HttpClient.postImage( htcmd: String, uin: Long, groupcode: Long?, - imageInput: ExternalImage.ReusableInput, // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor + imageInput: ReusableInput, uKeyHex: String ): Boolean = post { url { @@ -90,7 +92,7 @@ internal object HighwayHelper { bot: QQAndroidBot, servers: List>, uKey: ByteArray, - image: ExternalImage.ReusableInput, + image: ReusableInput, kind: String, commandId: Int ) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId) @@ -102,7 +104,7 @@ internal object HighwayHelper { servers: List>, uKey: ByteArray, md5: ByteArray, - input: ExternalImage.ReusableInput, + input: ReusableInput, kind: String, commandId: Int ) = servers.retryWithServers( @@ -139,7 +141,7 @@ internal object HighwayHelper { serverIp: String, serverPort: Int, ticket: ByteArray, - imageInput: ExternalImage.ReusableInput, + imageInput: ReusableInput, fileMd5: ByteArray, commandId: Int // group=2, friend=1 ) { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt index 59214a586..6a8e174c6 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt @@ -21,10 +21,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.utils.ByteArrayPool import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray -import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.internal.ChunkedFlowSession import net.mamoe.mirai.utils.internal.ChunkedInput +import net.mamoe.mirai.utils.internal.ReusableInput import net.mamoe.mirai.utils.internal.map @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) @@ -37,7 +37,7 @@ internal fun createImageDataPacketSequence( commandId: Int, localId: Int = 2052, ticket: ByteArray, - data: ExternalImage.ReusableInput, + data: ReusableInput, fileMd5: ByteArray, sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE ): ChunkedFlowSession { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Mirai.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Mirai.kt deleted file mode 100644 index 32edf24bb..000000000 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Mirai.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 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 - */ - -package net.mamoe.mirai - -import io.ktor.utils.io.ByteReadChannel -import kotlinx.io.core.Input -import net.mamoe.mirai.utils.ExternalImage -import net.mamoe.mirai.utils.MiraiExperimentalAPI -import net.mamoe.mirai.utils.SinceMirai -import net.mamoe.mirai.utils.internal.InputStream -import kotlin.jvm.JvmStatic - -/** - * Mirai 全局环境. - */ -@SinceMirai("1.0.0") -expect object Mirai { - - @JvmStatic - var fileCacheStrategy: FileCacheStrategy - - /** - * 缓存策略. - * - * 图片上传时默认使用文件缓存. - */ - interface FileCacheStrategy { - @MiraiExperimentalAPI - fun newImageCache(input: Input): ExternalImage - - @MiraiExperimentalAPI - fun newImageCache(input: ByteReadChannel): ExternalImage - - @MiraiExperimentalAPI - fun newImageCache(input: InputStream): ExternalImage - - companion object Default : FileCacheStrategy - } -} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt index a27145b2d..af2f1fc6a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.utils +import kotlinx.coroutines.Job import net.mamoe.mirai.Bot import net.mamoe.mirai.network.BotNetworkHandler import kotlin.coroutines.CoroutineContext @@ -24,30 +25,20 @@ import kotlin.jvm.JvmStatic */ @Suppress("PropertyName") open class BotConfiguration { - /** - * 日志记录器 - */ + /** 日志记录器 */ var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.id})") } - /** - * 网络层日志构造器 - */ + /** 网络层日志构造器 */ @OptIn(MiraiInternalAPI::class) var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.id})") } - /** - * 设备信息覆盖. 默认使用随机的设备信息. - */ + /** 设备信息覆盖. 默认使用随机的设备信息. */ var deviceInfo: ((Context) -> DeviceInfo)? = null - /** - * 父 [CoroutineContext] - */ + /** 父 [CoroutineContext]. [Bot] 创建后会覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */ var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext - /** - * 心跳周期. 过长会导致被服务器断开连接. - */ + /** 心跳周期. 过长会导致被服务器断开连接. */ var heartbeatPeriodMillis: Long = 60.secondsToMillis /** @@ -56,31 +47,26 @@ open class BotConfiguration { */ var heartbeatTimeoutMillis: Long = 2.secondsToMillis - /** - * 心跳失败后的第一次重连前的等待时间. - */ + /** 心跳失败后的第一次重连前的等待时间. */ var firstReconnectDelayMillis: Long = 5.secondsToMillis - /** - * 重连失败后, 继续尝试的每次等待时间 - */ + /** 重连失败后, 继续尝试的每次等待时间 */ var reconnectPeriodMillis: Long = 5.secondsToMillis - /** - * 最多尝试多少次重连 - */ + /** 最多尝试多少次重连 */ var reconnectionRetryTimes: Int = Int.MAX_VALUE - /** - * 验证码处理器 - */ + /** 验证码处理器 */ var loginSolver: LoginSolver = LoginSolver.Default - /** - * 使用协议类型 - */ + /** 使用协议类型 */ @SinceMirai("1.0.0") - val protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD + var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PAD + + /** 缓存策略 */ + @SinceMirai("1.0.0") + @MiraiExperimentalAPI + var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault @SinceMirai("1.0.0") enum class MiraiProtocol( @@ -105,9 +91,7 @@ open class BotConfiguration { } companion object { - /** - * 默认的配置实例 - */ + /** 默认的配置实例. 可以进行修改 */ @JvmStatic val Default = BotConfiguration() } @@ -144,11 +128,31 @@ open class BotConfiguration { * ``` */ @ConfigurationDsl - suspend fun inheritCoroutineContext() { + suspend inline fun inheritCoroutineContext() { parentCoroutineContext = coroutineContext } + @DslMarker annotation class ConfigurationDsl + + @SinceMirai("1.0.0") + fun copy(): BotConfiguration { + @OptIn(MiraiExperimentalAPI::class) + return BotConfiguration().also { new -> + new.botLoggerSupplier = botLoggerSupplier + new.networkLoggerSupplier = networkLoggerSupplier + new.deviceInfo = deviceInfo + new.parentCoroutineContext = parentCoroutineContext + new.heartbeatPeriodMillis = heartbeatPeriodMillis + new.heartbeatTimeoutMillis = heartbeatTimeoutMillis + new.firstReconnectDelayMillis = firstReconnectDelayMillis + new.reconnectPeriodMillis = reconnectPeriodMillis + new.reconnectionRetryTimes = reconnectionRetryTimes + new.loginSolver = loginSolver + new.protocol = protocol + new.fileCacheStrategy = fileCacheStrategy + } + } } @OptIn(ExperimentalMultiplatform::class) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt index 9e527093d..93dd93a35 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt @@ -11,15 +11,14 @@ package net.mamoe.mirai.utils -import io.ktor.utils.io.ByteWriteChannel import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.User import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.sendTo -import net.mamoe.mirai.utils.internal.ChunkedFlowSession -import net.mamoe.mirai.utils.internal.ChunkedInput +import net.mamoe.mirai.utils.internal.DeferredReusableInput +import net.mamoe.mirai.utils.internal.ReusableInput import kotlin.jvm.JvmField import kotlin.jvm.JvmSynthetic @@ -31,24 +30,16 @@ import kotlin.jvm.JvmSynthetic * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人 * @See ExternalImage.upload 上传图片并得到 [Image] 消息 */ -@OptIn(MiraiInternalAPI::class) class ExternalImage internal constructor( @JvmField - internal val input: ReusableInput // Input from kotlinx.io, InputStream from kotlinx.io MPP, ByteReadChannel from ktor + internal val input: ReusableInput ) { - val md5: ByteArray get() = this.input.md5 - - @SinceMirai("1.0.0") - internal interface ReusableInput { - val md5: ByteArray - val size: Long - - fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession - suspend fun writeTo(out: ByteWriteChannel): Long - } + internal val md5: ByteArray get() = this.input.md5 init { - require(input.size < 30L * 1024 * 1024) { "Image file is too big. Maximum is 30 MiB, but recommended to be 20 MiB" } + if (input !is DeferredReusableInput) { + require(input.size < 30L * 1024 * 1024) { "Image file is too big. Maximum is 30 MiB, but recommended to be 20 MiB" } + } } companion object { @@ -75,10 +66,16 @@ class ExternalImage internal constructor( * SHARPP: 1004 */ + override fun toString(): String { + if (input is DeferredReusableInput) { + if (!input.initialized) { + return "ExternalImage(uninitialized)" + } + } + return "ExternalImage(${generateUUID(md5)})" + } - override fun toString(): String = "[ExternalImage(${generateUUID(md5)})]" - - fun calculateImageResourceId(): String = generateImageId(md5) + internal fun calculateImageResourceId(): String = generateImageId(md5) } /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/FileCacheStrategy.common.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/FileCacheStrategy.common.kt new file mode 100644 index 000000000..f18ab7d6f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/FileCacheStrategy.common.kt @@ -0,0 +1,60 @@ +package net.mamoe.mirai.utils + +import kotlinx.io.core.Input +import kotlinx.io.errors.IOException +import net.mamoe.mirai.utils.internal.InputStream + +/** + * 缓存策略. + * + * 图片上传时默认使用文件缓存. + */ +@MiraiExperimentalAPI +expect interface FileCacheStrategy { + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此函数应 close 这个 [Input] + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + fun newImageCache(input: Input): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此函数应 close 这个 [InputStream] + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + fun newImageCache(input: InputStream): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此 [input] 的内容应是不变的. + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + fun newImageCache(input: ByteArray): ExternalImage + + /** + * 默认的缓存方案. 在 JVM 平台使用系统临时文件. + */ + @MiraiExperimentalAPI + object PlatformDefault : FileCacheStrategy + + /** + * 使用内存直接存储所有图片文件. + */ + object MemoryCache : FileCacheStrategy { + @MiraiExperimentalAPI + @Throws(IOException::class) + override fun newImageCache(input: Input): ExternalImage + + @MiraiExperimentalAPI + @Throws(IOException::class) + override fun newImageCache(input: InputStream): ExternalImage + + @MiraiExperimentalAPI + @Throws(IOException::class) + override fun newImageCache(input: ByteArray): ExternalImage + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/DeferredReusableInput.common.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/DeferredReusableInput.common.kt new file mode 100644 index 000000000..bd0016dad --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/DeferredReusableInput.common.kt @@ -0,0 +1,11 @@ +package net.mamoe.mirai.utils.internal + +import net.mamoe.mirai.utils.FileCacheStrategy +import net.mamoe.mirai.utils.MiraiExperimentalAPI + +internal expect class DeferredReusableInput(input: Any, extraArg: Any?) : ReusableInput { + val initialized: Boolean + + @OptIn(MiraiExperimentalAPI::class) + suspend fun init(strategy: FileCacheStrategy) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/ReusableInput.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/ReusableInput.kt new file mode 100644 index 000000000..9eab67eb4 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/ReusableInput.kt @@ -0,0 +1,13 @@ +package net.mamoe.mirai.utils.internal + +import io.ktor.utils.io.ByteWriteChannel +import net.mamoe.mirai.utils.SinceMirai + +@SinceMirai("1.0.0") +internal interface ReusableInput { + val md5: ByteArray + val size: Long + + fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession + suspend fun writeTo(out: ByteWriteChannel): Long +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/asReusableInput.common.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/asReusableInput.common.kt index 52a4ded47..abcf9c24a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/asReusableInput.common.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/internal/asReusableInput.common.kt @@ -9,9 +9,6 @@ package net.mamoe.mirai.utils.internal -import net.mamoe.mirai.utils.ExternalImage +internal expect fun ByteArray.asReusableInput(): ReusableInput - -internal expect fun ByteArray.asReusableInput(): ExternalImage.ReusableInput - -internal fun asReusableInput0(input: ByteArray): ExternalImage.ReusableInput = input.asReusableInput() \ No newline at end of file +internal fun asReusableInput0(input: ByteArray): ReusableInput = input.asReusableInput() \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Mirai.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Mirai.kt deleted file mode 100644 index d584a918a..000000000 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Mirai.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.mamoe.mirai - -import io.ktor.utils.io.ByteReadChannel -import kotlinx.io.core.Input -import net.mamoe.mirai.utils.ExternalImage -import net.mamoe.mirai.utils.MiraiExperimentalAPI -import net.mamoe.mirai.utils.internal.InputStream - -/** - * Mirai 全局环境. - */ -actual object Mirai { - actual var fileCacheStrategy: FileCacheStrategy - get() = TODO("Not yet implemented") - set(value) {} - - actual interface FileCacheStrategy { - @MiraiExperimentalAPI - actual fun newImageCache(input: Input): ExternalImage - - @MiraiExperimentalAPI - actual fun newImageCache(input: ByteReadChannel): ExternalImage - - @MiraiExperimentalAPI - actual fun newImageCache(input: InputStream): ExternalImage - - actual companion object Default : FileCacheStrategy { - @MiraiExperimentalAPI - actual override fun newImageCache(input: Input): ExternalImage { - TODO("Not yet implemented") - } - - @MiraiExperimentalAPI - actual override fun newImageCache(input: ByteReadChannel): ExternalImage { - TODO("Not yet implemented") - } - - @MiraiExperimentalAPI - actual override fun newImageCache(input: InputStream): ExternalImage { - TODO("Not yet implemented") - } - } - - } - -} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt index c01a42aa9..d540d2859 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt @@ -37,7 +37,7 @@ import java.net.URL */ @Throws(OverFileSizeMaxException::class) suspend fun BufferedImage.sendTo(contact: C): MessageReceipt = - withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) + toExternalImage().sendTo(contact) /** * 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人 @@ -45,7 +45,7 @@ suspend fun BufferedImage.sendTo(contact: C): MessageReceipt = */ @Throws(OverFileSizeMaxException::class) suspend fun URL.sendAsImageTo(contact: C): MessageReceipt = - withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) + toExternalImage().sendTo(contact) /** * 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人 @@ -53,7 +53,7 @@ suspend fun URL.sendAsImageTo(contact: C): MessageReceipt = */ @Throws(OverFileSizeMaxException::class) suspend fun Input.sendAsImageTo(contact: C): MessageReceipt = - withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) + toExternalImage().sendTo(contact) /** * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 @@ -61,7 +61,7 @@ suspend fun Input.sendAsImageTo(contact: C): MessageReceipt = */ @Throws(OverFileSizeMaxException::class) suspend fun InputStream.sendAsImageTo(contact: C): MessageReceipt = - withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) + toExternalImage().sendTo(contact) /** * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人 @@ -70,7 +70,7 @@ suspend fun InputStream.sendAsImageTo(contact: C): MessageReceipt< @Throws(OverFileSizeMaxException::class) suspend fun File.sendAsImageTo(contact: C): MessageReceipt { require(this.exists() && this.canRead()) - return withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact) + return toExternalImage().sendTo(contact) } // endregion @@ -84,7 +84,7 @@ suspend fun File.sendAsImageTo(contact: C): MessageReceipt { @JvmSynthetic @Throws(OverFileSizeMaxException::class) suspend fun BufferedImage.upload(contact: Contact): Image = - withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) + toExternalImage().upload(contact) /** * 在 [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image] @@ -92,7 +92,7 @@ suspend fun BufferedImage.upload(contact: Contact): Image = */ @Throws(OverFileSizeMaxException::class) suspend fun URL.uploadAsImage(contact: Contact): Image = - withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) + toExternalImage().upload(contact) /** * 在 [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image] @@ -100,7 +100,7 @@ suspend fun URL.uploadAsImage(contact: Contact): Image = */ @Throws(OverFileSizeMaxException::class) suspend fun Input.uploadAsImage(contact: Contact): Image = - withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) + toExternalImage().upload(contact) /** * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image] @@ -108,7 +108,7 @@ suspend fun Input.uploadAsImage(contact: Contact): Image = */ @Throws(OverFileSizeMaxException::class) suspend fun InputStream.uploadAsImage(contact: Contact): Image = - withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) + toExternalImage().upload(contact) /** * 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image] @@ -117,7 +117,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): Image = @Throws(OverFileSizeMaxException::class) suspend fun File.uploadAsImage(contact: Contact): Image { require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" } - return withContext(Dispatchers.IO) { toExternalImage() }.upload(contact) + return toExternalImage().upload(contact) } // endregion diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt index 5f0e9746a..85bc40311 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt @@ -11,22 +11,14 @@ package net.mamoe.mirai.utils -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.io.ByteReadChannel -import kotlinx.coroutines.withContext import kotlinx.io.core.Input -import kotlinx.io.core.copyTo -import kotlinx.io.errors.IOException -import kotlinx.io.streams.asOutput +import net.mamoe.mirai.Bot +import net.mamoe.mirai.utils.internal.DeferredReusableInput import net.mamoe.mirai.utils.internal.asReusableInput -import net.mamoe.mirai.utils.internal.md5 import java.awt.image.BufferedImage import java.io.File import java.io.InputStream -import java.io.OutputStream import java.net.URL -import java.security.MessageDigest -import javax.imageio.ImageIO /* * 将各类型图片容器转为 [ExternalImage] @@ -34,126 +26,55 @@ import javax.imageio.ImageIO /** - * 将 [BufferedImage] 保存稳临时文件, 然后构造 [ExternalImage] + * 将 [BufferedImage] 保存为临时文件, 然后构造 [ExternalImage] */ @JvmOverloads -@Throws(IOException::class) -fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage { - val file = createTempFile().apply { deleteOnExit() } - - val digest = MessageDigest.getInstance("md5") - digest.reset() - - file.outputStream().use { out -> - ImageIO.write(this@toExternalImage, formatName, 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()) -} - -suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } +fun BufferedImage.toExternalImage(formatName: String = "png"): ExternalImage = + ExternalImage(DeferredReusableInput(this, formatName)) /** - * 直接使用文件 [inputStream] 构造 [ExternalImage] + * 将文件作为 [ExternalImage] 使用. 只会在需要的时候打开文件并读取数据. + * @param deleteOnClose 若为 `true`, 图片发送后将会删除这个文件 */ -@OptIn(MiraiInternalAPI::class) -@Throws(IOException::class) -fun File.toExternalImage(): ExternalImage { - @Suppress("DEPRECATION_ERROR") - return ExternalImage( - input = this.asReusableInput() - ) -} +@JvmOverloads +fun File.toExternalImage(deleteOnClose: Boolean = false): ExternalImage = ExternalImage(asReusableInput(deleteOnClose)) /** - * 在 [IO] 中进行 [File.toExternalImage] + * 将 [URL] 委托为 [ExternalImage]. + * 只会在上传图片时才读取 [URL] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy] */ -suspend inline fun File.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } +fun URL.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null)) /** - * 下载文件到临时目录然后调用 [File.toExternalImage] + * 将 [InputStream] 委托为 [ExternalImage]. + * 只会在上传图片时才读取 [InputStream] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy] */ -@Throws(IOException::class) -fun URL.toExternalImage(): ExternalImage { - val file = createTempFile().apply { deleteOnExit() } - file.outputStream().use { output -> - openStream().use { input -> - input.copyTo(output) - } - output.flush() - } - return file.toExternalImage() -} +@JvmName("toExternalImage") +fun InputStream.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null)) /** - * 在 [IO] 中进行 [URL.toExternalImage] + * 将 [Input] 委托为 [ExternalImage]. + * 只会在上传图片时才读取 [Input] 的内容. 具体行为取决于相关 [Bot] 的 [FileCacheStrategy] */ -suspend inline fun URL.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } +fun Input.toExternalImage(): ExternalImage = ExternalImage(DeferredReusableInput(this, null)) -/** - * 保存为临时文件然后调用 [File.toExternalImage] - */ -@Throws(IOException::class) -fun InputStream.toExternalImage(): ExternalImage { - val file = createTempFile().apply { deleteOnExit() } - file.outputStream().use { - this.copyTo(it) - it.flush() - } - this.close() - return file.toExternalImage() -} -/** - * 在 [IO] 中进行 [InputStream.toExternalImage] - */ -suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } +@PlannedRemoval("1.2.0") +@Deprecated("no need", ReplaceWith("toExternalImage()")) +fun Input.suspendToExternalImage(): ExternalImage = toExternalImage() -/** - * 保存为临时文件然后调用 [File.toExternalImage]. - * - * 需要函数调用者 close [this] - */ -@Throws(IOException::class) -fun Input.toExternalImage(): ExternalImage { - val file = createTempFile().apply { deleteOnExit() } - file.outputStream().asOutput().use { - this.copyTo(it) - it.flush() - } - return file.toExternalImage() -} +@PlannedRemoval("1.2.0") +@Deprecated("no need", ReplaceWith("toExternalImage()")) +fun InputStream.suspendToExternalImage(): ExternalImage = toExternalImage() -/** - * 在 [IO] 中进行 [Input.toExternalImage] - */ -suspend inline fun Input.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() } +@PlannedRemoval("1.2.0") +@Deprecated("no need", ReplaceWith("toExternalImage()")) +fun URL.suspendToExternalImage(): ExternalImage = toExternalImage() -/** - * 保存为临时文件然后调用 [File.toExternalImage]. - */ -suspend fun ByteReadChannel.toExternalImage(): ExternalImage { - val file = createTempFile().apply { deleteOnExit() } - file.outputStream().use { - withContext(IO) { copyTo(it) } - it.flush() - } +@PlannedRemoval("1.2.0") +@Deprecated("no need", ReplaceWith("toExternalImage()")) +fun File.suspendToExternalImage(): ExternalImage = toExternalImage() - return file.suspendToExternalImage() -} \ No newline at end of file +@PlannedRemoval("1.2.0") +@Deprecated("no need", ReplaceWith("toExternalImage()")) +fun BufferedImage.suspendToExternalImage(): ExternalImage = toExternalImage() diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/FileCacheStrategy.jvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/FileCacheStrategy.jvm.kt new file mode 100644 index 000000000..2385ae4af --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/FileCacheStrategy.jvm.kt @@ -0,0 +1,213 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.utils + +import kotlinx.io.core.Closeable +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.InputStream +import net.mamoe.mirai.utils.internal.asReusableInput +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.URL +import java.security.MessageDigest +import javax.imageio.ImageIO +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * 缓存策略. + * + * 图片上传时默认使用文件缓存. + * + * @see BotConfiguration.fileCacheStrategy 为 [Bot] 指定缓存策略 + */ +@MiraiExperimentalAPI +actual interface FileCacheStrategy { + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此函数应 close 这个 [Input] + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + actual fun newImageCache(input: Input): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此函数应 close 这个 [InputStream] + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + actual fun newImageCache(input: InputStream): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此 [input] 的内容应是不变的. + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + actual fun newImageCache(input: ByteArray): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + * 此 [input] 的内容应是不变的. + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage + + /** + * 将 [input] 缓存为 [ExternalImage]. + */ + @MiraiExperimentalAPI + @Throws(IOException::class) + fun newImageCache(input: URL, format: String = "png"): ExternalImage + + /** + * 默认的缓存方案, 使用系统临时文件夹存储. + */ + @MiraiExperimentalAPI + actual object PlatformDefault : FileCacheStrategy by TempCache(null) + + /** + * 使用内存直接存储所有图片文件. + */ + actual object MemoryCache : FileCacheStrategy { + @MiraiExperimentalAPI + @Throws(IOException::class) + actual override fun newImageCache(input: Input): ExternalImage { + return newImageCache(input.readBytes()) + } + + @MiraiExperimentalAPI + @Throws(IOException::class) + actual 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, format: String): ExternalImage { + val out = ByteArrayOutputStream() + input.openConnection().getInputStream().use { it.copyTo(out) } + return newImageCache(out.toByteArray()) + } + } + + /** + * 使用系统临时文件夹缓存图片文件. 在图片使用完毕后删除临时文件. + */ + @MiraiExperimentalAPI + class TempCache @JvmOverloads constructor( + /** + * 缓存图片存放位置 + */ + 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, format: String): ExternalImage { + return ExternalImage(createTempFile(directory = directory).apply { + deleteOnExit() + input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) } + }.asReusableInput(true)) + } + } +} + +@OptIn(ExperimentalContracts::class) +internal inline fun I.withOut(output: O, block: I.(output: O) -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return use { output.use { block(this, output) } } +} + +/** + * Copies this stream to the given output stream, returning the number of bytes copied + * + * **Note** It is the caller's responsibility to close both of these resources. + */ +@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 +} diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/DeferredReusableInput.jvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/DeferredReusableInput.jvm.kt new file mode 100644 index 000000000..974acb445 --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/DeferredReusableInput.jvm.kt @@ -0,0 +1,49 @@ +package net.mamoe.mirai.utils.internal + +import io.ktor.utils.io.ByteWriteChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.io.core.Input +import net.mamoe.mirai.utils.FileCacheStrategy +import net.mamoe.mirai.utils.MiraiExperimentalAPI +import java.awt.image.BufferedImage +import java.net.URL + +internal actual class DeferredReusableInput actual constructor( + val input: Any, + val extraArg: Any? +) : ReusableInput { + + + @OptIn(MiraiExperimentalAPI::class) + actual suspend fun init(strategy: FileCacheStrategy) = withContext(Dispatchers.IO) { + if (delegate != null) { + return@withContext + } + delegate = when (input) { + is InputStream -> strategy.newImageCache(input) + is ByteArray -> strategy.newImageCache(input) + is Input -> strategy.newImageCache(input) + is BufferedImage -> strategy.newImageCache(input, extraArg as String) + is URL -> strategy.newImageCache(input) + else -> error("Internal error: unsupported DeferredReusableInput.input: ${input::class.qualifiedName}") + }.input + } + + private var delegate: ReusableInput? = null + + override val md5: ByteArray + get() = delegate?.md5 ?: error("DeferredReusableInput not yet initialized") + override val size: Long + get() = delegate?.size ?: error("DeferredReusableInput not yet initialized") + + override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession { + return delegate?.chunkedFlow(sizePerPacket) ?: error("DeferredReusableInput not yet initialized") + } + + override suspend fun writeTo(out: ByteWriteChannel): Long { + return delegate?.writeTo(out) ?: error("DeferredReusableInput not yet initialized") + } + + actual val initialized: Boolean get() = delegate != null +} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/asReusableInput.jvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/asReusableInput.jvm.kt index 23ed10be3..168b043f8 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/asReusableInput.jvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/internal/asReusableInput.jvm.kt @@ -5,12 +5,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import net.mamoe.mirai.message.data.toLongUnsigned -import net.mamoe.mirai.utils.ExternalImage import java.io.File import java.io.InputStream -internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput { - return object : ExternalImage.ReusableInput { +internal actual fun ByteArray.asReusableInput(): ReusableInput { + return object : ReusableInput { override val md5: ByteArray = md5() override val size: Long get() = this@asReusableInput.size.toLongUnsigned() @@ -32,8 +31,8 @@ internal actual fun ByteArray.asReusableInput(): ExternalImage.ReusableInput { } } -internal fun File.asReusableInput(): ExternalImage.ReusableInput { - return object : ExternalImage.ReusableInput { +internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput { + return object : ReusableInput { override val md5: ByteArray = inputStream().use { it.md5() } override val size: Long get() = length() @@ -41,7 +40,10 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput { val stream = inputStream() return object : ChunkedFlowSession { override val flow: Flow = stream.chunkedFlow(sizePerPacket) - override fun close() = stream.close() + override fun close() { + stream.close() + if (deleteOnClose) this@asReusableInput.delete() + } } } @@ -51,6 +53,27 @@ internal fun File.asReusableInput(): ExternalImage.ReusableInput { } } +internal fun File.asReusableInput(deleteOnClose: Boolean, md5: ByteArray): ReusableInput { + return object : ReusableInput { + override val md5: ByteArray get() = md5 + override val size: Long get() = length() + + override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession { + val stream = inputStream() + return object : ChunkedFlowSession { + override val flow: Flow = stream.chunkedFlow(sizePerPacket) + override fun close() { + stream.close() + if (deleteOnClose) this@asReusableInput.delete() + } + } + } + + override suspend fun writeTo(out: ByteWriteChannel): Long { + return inputStream().use { it.copyTo(out) } + } + } +} private suspend fun InputStream.copyTo(out: ByteWriteChannel): Long = withContext(Dispatchers.IO) { var bytesCopied: Long = 0