diff --git a/mirai-core-api/src/commonMain/kotlin/IMirai.kt b/mirai-core-api/src/commonMain/kotlin/IMirai.kt index 665283921..e29e958a3 100644 --- a/mirai-core-api/src/commonMain/kotlin/IMirai.kt +++ b/mirai-core-api/src/commonMain/kotlin/IMirai.kt @@ -23,6 +23,7 @@ import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.Image.Key.queryUrl import net.mamoe.mirai.message.data.MessageSource.Key.recall +import net.mamoe.mirai.utils.FileCacheStrategy import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiInternalApi @@ -35,7 +36,7 @@ public val Mirai: IMirai by lazy { findMiraiInstance() } /** * Mirai API 接口. * - * @see Mirai + * @see Mirai 获取实例 */ public interface IMirai : LowLevelApiAccessor { /** @@ -47,6 +48,13 @@ public interface IMirai : LowLevelApiAccessor { @MiraiExperimentalApi public val BotFactory: BotFactory + /** + * Mirai 全局使用的 [FileCacheStrategy]. + */ + @Suppress("PropertyName") + @MiraiExperimentalApi + public var FileCacheStrategy: FileCacheStrategy + /** * 使用 groupCode 计算 groupUin. 这两个值仅在 mirai 内部协议区分, 一般人使用时无需在意. */ diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt index 72a13e9a8..69a9d1c09 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt @@ -20,9 +20,9 @@ import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt.Companion.quote import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.ExternalImage -import net.mamoe.mirai.utils.OverFileSizeMaxException -import net.mamoe.mirai.utils.WeakRefProperty +import net.mamoe.mirai.utils.* +import java.io.File +import java.io.InputStream /** * 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], 和 [群][Group]. @@ -70,21 +70,51 @@ public interface Contact : ContactOrBot, CoroutineScope { /** * 上传一个图片以备发送. * + * 无论上传是否成功都不会关闭 [resource]. + * * @see Image 查看有关图片的更多信息, 如上传图片 * * @see BeforeImageUploadEvent 图片发送前事件, 可拦截. * @see ImageUploadEvent 图片发送完成事件, 不可拦截. * + * @see ExternalResource + * * @throws EventCancelledException 当发送消息事件被取消时抛出 * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, 但 mirai 限制的大小为 30 MB) */ @JvmBlockingBridge - public suspend fun uploadImage(image: ExternalImage): Image + public suspend fun uploadImage(resource: ExternalResource): Image /** * @return "Friend($id)" or "Group($id)" or "Member($id)" */ public override fun toString(): String + + public companion object { + /** + * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 + * + * 注意:此函数不会关闭 [imageStream] + * + * @throws OverFileSizeMaxException + * @see FileCacheStrategy + */ + @Throws(OverFileSizeMaxException::class) + @JvmStatic + @JvmBlockingBridge + public suspend fun C.sendImage(imageStream: InputStream): MessageReceipt = + imageStream.sendAsImageTo(this) + + /** + * 将文件作为图片发送到指定联系人 + * @throws OverFileSizeMaxException + * @see FileCacheStrategy + */ + @Throws(OverFileSizeMaxException::class) + @JvmStatic + @JvmBlockingBridge + public suspend fun C.sendImage(file: File): MessageReceipt = file.sendAsImageTo(this) + } } /** diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt index cecbc676a..603488d9e 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt @@ -18,7 +18,6 @@ import net.mamoe.mirai.event.events.* import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.OverFileSizeMaxException import net.mamoe.mirai.utils.PlannedRemoval @@ -162,21 +161,6 @@ public interface Group : Contact, CoroutineScope { public override suspend fun sendMessage(message: String): MessageReceipt = this.sendMessage(message.toPlainText()) - - /** - * 上传一个图片以备发送. - * - * @see Image 查看有关图片的更多信息, 如上传图片 - * - * @see BeforeImageUploadEvent 图片上传前事件, cancellable - * @see ImageUploadEvent 图片上传完成事件 - * - * @throws EventCancelledException 当发送消息事件被取消 - * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB) - */ - @JvmBlockingBridge - public override suspend fun uploadImage(image: ExternalImage): Image - /** * 上传一个语音消息以备发送. * 请手动关闭输入流 diff --git a/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt b/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt index 7eae0c135..8f3777050 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/OtherClient.kt @@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE -import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiInternalApi @@ -42,7 +42,7 @@ public interface OtherClient : Contact { throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.") } - override suspend fun uploadImage(image: ExternalImage): Image { + override suspend fun uploadImage(resource: ExternalResource): Image { throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.") } } diff --git a/mirai-core-api/src/commonMain/kotlin/contact/User.kt b/mirai-core-api/src/commonMain/kotlin/contact/User.kt index 1ae8f4950..284993b52 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/User.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/User.kt @@ -14,18 +14,17 @@ package net.mamoe.mirai.contact import kotlinx.coroutines.CoroutineScope import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.Bot -import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.event.events.EventCancelledException +import net.mamoe.mirai.event.events.UserMessagePostSendEvent +import net.mamoe.mirai.event.events.UserMessagePreSendEvent import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.action.UserNudge -import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.isContentEmpty import net.mamoe.mirai.message.data.toPlainText -import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.MiraiExperimentalApi -import net.mamoe.mirai.utils.OverFileSizeMaxException /** * 代表一个 **用户**. @@ -92,20 +91,6 @@ public interface User : Contact, UserOrBot, CoroutineScope { */ @MiraiExperimentalApi public override fun nudge(): UserNudge - - /** - * 上传一个图片以备发送. - * - * @see Image 查看有关图片的更多信息, 如上传图片 - * - * @see BeforeImageUploadEvent 图片发送前事件, cancellable - * @see ImageUploadEvent 图片发送完成事件 - * - * @throws EventCancelledException 当发送消息事件被取消 - * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB) - */ - @JvmBlockingBridge - public override suspend fun uploadImage(image: ExternalImage): Image } /** diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/message.kt b/mirai-core-api/src/commonMain/kotlin/event/events/message.kt index 26d02b025..abe9c6ee5 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/message.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/message.kt @@ -16,6 +16,7 @@ package net.mamoe.mirai.event.events import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* +import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.event.* import net.mamoe.mirai.event.events.ImageUploadEvent.Failed import net.mamoe.mirai.event.events.ImageUploadEvent.Succeed @@ -26,7 +27,8 @@ import net.mamoe.mirai.message.data.Image.Key.queryUrl import net.mamoe.mirai.message.data.MessageSource.Key.quote import net.mamoe.mirai.message.isContextIdenticalWith import net.mamoe.mirai.utils.* -import java.awt.image.BufferedImage +import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo +import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage import java.io.File import java.io.InputStream import kotlin.internal.InlineOnly @@ -415,7 +417,7 @@ public val MessageRecallEvent.isByBot: Boolean */ public data class BeforeImageUploadEvent @MiraiInternalApi constructor( public val target: Contact, - public val source: ExternalImage + public val source: ExternalResource ) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent { public override val bot: Bot get() = target.bot @@ -434,19 +436,19 @@ public data class BeforeImageUploadEvent @MiraiInternalApi constructor( */ public sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractEvent() { public abstract val target: Contact - public abstract val source: ExternalImage + public abstract val source: ExternalResource public override val bot: Bot get() = target.bot public data class Succeed @MiraiInternalApi constructor( override val target: Contact, - override val source: ExternalImage, + override val source: ExternalResource, val image: Image ) : ImageUploadEvent() public data class Failed @MiraiInternalApi constructor( override val target: Contact, - override val source: ExternalImage, + override val source: ExternalResource, val errno: Int, val message: String ) : ImageUploadEvent() @@ -631,9 +633,9 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() { public override suspend fun reply(plain: String): MessageReceipt = subject.sendMessage(PlainText(plain).asMessageChain()) - public override suspend fun ExternalImage.upload(): Image = this.upload(subject) + public override suspend fun ExternalResource.uploadAsImage(): Image = this.uploadAsImage(subject) - public override suspend fun ExternalImage.send(): MessageReceipt = this.sendTo(subject) + public override suspend fun ExternalResource.sendAsImage(): MessageReceipt = this.sendAsImageTo(subject) public override suspend fun Image.send(): MessageReceipt = this.sendTo(subject) @@ -667,25 +669,21 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() { // region 上传图片 - public override suspend fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image) public override suspend fun uploadImage(image: InputStream): Image = subject.uploadImage(image) public override suspend fun uploadImage(image: File): Image = subject.uploadImage(image) // endregion // region 发送图片 - public override suspend fun sendImage(image: BufferedImage): MessageReceipt = subject.sendImage(image) public override suspend fun sendImage(image: InputStream): MessageReceipt = subject.sendImage(image) public override suspend fun sendImage(image: File): MessageReceipt = subject.sendImage(image) // endregion // region 上传图片 (扩展) - public override suspend fun BufferedImage.upload(): Image = upload(subject) public override suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject) public override suspend fun File.uploadAsImage(): Image = uploadAsImage(subject) // endregion 上传图片 (扩展) // region 发送图片 (扩展) - public override suspend fun BufferedImage.send(): MessageReceipt = sendTo(subject) public override suspend fun InputStream.sendAsImage(): MessageReceipt = sendAsImageTo(subject) public override suspend fun File.sendAsImage(): MessageReceipt = sendAsImageTo(subject) // endregion 发送图片 (扩展) @@ -773,10 +771,10 @@ public interface MessageEventExtensions + public suspend fun ExternalResource.sendAsImage(): MessageReceipt @JvmSynthetic public suspend fun Image.send(): MessageReceipt @@ -828,9 +826,6 @@ public interface MessageEventPlatformExtensions - @JvmBlockingBridge public suspend fun sendImage(image: InputStream): MessageReceipt @@ -850,9 +842,6 @@ public interface MessageEventPlatformExtensions - @JvmSynthetic public suspend fun InputStream.sendAsImage(): MessageReceipt diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt b/mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt index 4ec8c4bda..68a194384 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/CustomMessage.kt @@ -19,7 +19,6 @@ import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoNumber import net.mamoe.mirai.message.MessageSerializer import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.internal.checkOffsetAndLength /** * 自定义消息 @@ -211,28 +210,3 @@ internal inline fun T.customToStringImpl(factory: Cu @Suppress("UNCHECKED_CAST") return (factory as CustomMessage.Factory).dump(this) } - -@OptIn(ExperimentalUnsignedTypes::class) -@JvmOverloads -@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray -internal fun ByteArray.toUHexString( - separator: String = " ", - offset: Int = 0, - length: Int = this.size - offset -): String { - this.checkOffsetAndLength(offset, length) - if (length == 0) { - return "" - } - val lastIndex = offset + length - return buildString(length * 2) { - this@toUHexString.forEachIndexed { index, it -> - if (index in offset until lastIndex) { - var ret = it.toUByte().toString(16).toUpperCase() - if (ret.length == 1) ret = "0$ret" - append(ret) - if (index < lastIndex - 1) append(separator) - } - } - } -} diff --git a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt index f90187035..482a434a9 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt @@ -112,10 +112,6 @@ public open class BotConfiguration { // open for Java */ public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub - /** 缓存策略 */ - @MiraiExperimentalApi - public var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault - /** * Json 序列化器, 使用 'kotlinx.serialization' */ @@ -270,7 +266,6 @@ public open class BotConfiguration { // open for Java new.reconnectionRetryTimes = reconnectionRetryTimes new.loginSolver = loginSolver new.protocol = protocol - new.fileCacheStrategy = fileCacheStrategy } } diff --git a/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt index 99d09c9c4..1d7201ea8 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt @@ -17,7 +17,6 @@ import kotlinx.serialization.protobuf.ProtoNumber import net.mamoe.mirai.utils.internal.getRandomByteArray import net.mamoe.mirai.utils.internal.getRandomIntString import net.mamoe.mirai.utils.internal.getRandomString -import net.mamoe.mirai.utils.internal.md5 import java.io.File /** @@ -78,7 +77,7 @@ public class DeviceInfo( model = "mirai".toByteArray(), bootloader = "unknown".toByteArray(), fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${getRandomIntString(7)}:user/release-keys".toByteArray(), - bootId = ExternalImage.generateUUID(getRandomByteArray(16).md5()).toByteArray(), + bootId = generateUUID(getRandomByteArray(16).md5()).toByteArray(), procVersion = "Linux version 3.0.31-${getRandomString(8)} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(), baseBand = byteArrayOf(), version = Version(), diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalImage.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalImage.kt deleted file mode 100644 index a0fcb1646..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalImage.kt +++ /dev/null @@ -1,162 +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.readBytes -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.message.data.toUHexString -import net.mamoe.mirai.utils.internal.DeferredReusableInput -import net.mamoe.mirai.utils.internal.ReusableInput -import java.io.File - -/** - * mirai 将在未来重构 [ExternalImage] 相关 API, 请尽量避免使用他们. - * - * 可以直接通过 [File.uploadAsImageTo] 等 API 替代. - */ -@RequiresOptIn( - "mirai 将在 2.0.0 时重构 ExternalImage 相关 API, 请尽量避免使用他们. 可以直接通过 File.uploadAsImageTo() 等 API 替代.", - level = RequiresOptIn.Level.WARNING -) -@Retention(AnnotationRetention.BINARY) -@UnstableExternalImage -public annotation class UnstableExternalImage - -/** - * 外部图片. 图片数据还没有读取到内存. - * - * 在 JVM, 请查看 'ExternalImageJvm.kt' - * - * @see ExternalImage.sendTo 上传图片并以纯图片消息发送给联系人 - * @See ExternalImage.upload 上传图片并得到 [Image] 消息 - */ -@UnstableExternalImage -public class ExternalImage internal constructor( - internal val input: ReusableInput -) { - internal val md5: ByteArray get() = input.md5 - public val formatName: String by lazy { - val hex = input.asInput().use { - it.readBytes(8).toUHexString("") - } - - return@lazy hex.detectFormatName() - } - - init { - 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" } - } - } - - public companion object { - public const val defaultFormatName: String = "mirai" - - - @MiraiExperimentalApi - public fun generateUUID(md5: ByteArray): String { - return "${md5[0, 3]}-${md5[4, 5]}-${md5[6, 7]}-${md5[8, 9]}-${md5[10, 15]}" - } - - @MiraiExperimentalApi - @JvmOverloads - public fun generateImageId(md5: ByteArray, format: String = defaultFormatName): String { - return """{${generateUUID(md5)}}.$format""" - } - } - - public override fun toString(): String { - if (input is DeferredReusableInput) { - if (!input.initialized) { - return "ExternalImage(uninitialized)" - } - } - return "ExternalImage(${generateUUID(md5)})" - } - - internal fun calculateImageResourceId(): String = generateImageId(md5, formatName) - - private fun String.detectFormatName(): String = when { - startsWith("FFD8") -> "jpg" - startsWith("89504E47") -> "png" - startsWith("47494638") -> "gif" - startsWith("424D") -> "bmp" - else -> defaultFormatName - } -} - -/* - * ImgType: - * JPG: 1000 - * PNG: 1001 - * WEBP: 1002 - * BMP: 1005 - * GIG: 2000 // gig? gif? - * APNG: 2001 - * SHARPP: 1004 - */ - -/** - * 将图片作为单独的消息发送给指定联系人. - * - * @see Contact.uploadImage 上传图片 - * @see Contact.sendMessage 最终调用, 发送消息. - */ -@JvmSynthetic -public suspend fun ExternalImage.sendTo(contact: C): MessageReceipt = when (contact) { - is Group -> contact.uploadImage(this).sendTo(contact) - is User -> contact.uploadImage(this).sendTo(contact) - else -> error("unreachable") -} - -/** - * 上传图片并构造 [Image]. - * 这个函数可能需消耗一段时间. - * - * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人 - * - * @see Contact.uploadImage 最终调用, 上传图片. - */ -@JvmSynthetic -public suspend fun ExternalImage.upload(contact: Contact): Image = when (contact) { - is Group -> contact.uploadImage(this) - is User -> contact.uploadImage(this) - else -> error("unreachable") -} - -/** - * 将图片作为单独的消息发送给 [this] - * - * @see Contact.sendMessage 最终调用, 发送消息. - */ -@JvmSynthetic -public suspend inline fun C.sendImage(image: ExternalImage): MessageReceipt = image.sendTo(this) - - -@JvmSynthetic -internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString { - for (it in rangeStart..rangeEnd) { - append(this@get[it].fixToString()) - } -} - -private fun Byte.fixToString(): String { - return when (val b = this.toInt() and 0xff) { - in 0..15 -> "0${this.toString(16).toUpperCase()}" - else -> b.toString(16).toUpperCase() - } -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalImageJvm.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalImageJvm.kt deleted file mode 100644 index fa4ae7ad5..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalImageJvm.kt +++ /dev/null @@ -1,54 +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 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)) \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt new file mode 100644 index 000000000..e73fb57f4 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -0,0 +1,272 @@ +/* + * 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.kjbb.JvmBlockingBridge +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.Contact.Companion.sendImage +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.ExternalResource.Companion.sendAsImageTo +import net.mamoe.mirai.utils.ExternalResource.Companion.sendImage +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource +import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage +import java.io.* + + +/** + * 一个*不可变的*外部资源. + * + * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据. + * + * ## 创建 + * - [File.toExternalResource] + * - [RandomAccessFile.toExternalResource] + * - [ByteArray.toExternalResource] + * - [InputStream.toExternalResource] + * + * ## 释放 + * + * 当 [ExternalResource] 创建时就可能会打开个文件 (如使用 [File.toExternalResource]). + * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close]. + * + * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image] + * @see ExternalResource.sendAsImageTo 将资源作为图片发送 + * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image] + * @see Contact.sendImage 发送一个资源作为图片 + * + * @see FileCacheStrategy + */ +public interface ExternalResource : Closeable { + + /** + * 文件内容 MD5. 16 bytes + */ + public val md5: ByteArray + + /** + * 文件格式,如 "png", "amr". 当无法自动识别格式时为 "mirai" + */ + public val formatName: String + + /** + * 文件大小 bytes + */ + public val size: Long + + /** + * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流. + * + * 关闭此流不会关闭 [ExternalResource]. + */ + public fun inputStream(): InputStream + + @MiraiInternalApi + public fun calculateResourceId(): String { + return generateImageId(md5, formatName.ifEmpty { "mirai" }) + } + + public companion object { + /** + * 在无法识别文件格式时使用的默认格式名. + * + * @see ExternalResource.formatName + */ + public const val DEFAULT_FORMAT_NAME: String = "mirai" + + /** + * **打开文件**并创建 [ExternalResource]. + * + * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close]. + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + public fun File.toExternalResource(formatName: String? = null): ExternalResource = + RandomAccessFile(this, "r").toExternalResource(formatName) + + /** + * 创建 [ExternalResource]. + * + * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会. + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + public fun RandomAccessFile.toExternalResource( + formatName: String? = null, + closeOriginalFileOnClose: Boolean = true + ): ExternalResource = + ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose) + + /** + * 创建 [ExternalResource] + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource = + ExternalResourceImplByByteArray(this, formatName) + + + /** + * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource]. + * + * 注意:本函数不会关闭流 + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + @Throws(IOException::class) + public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource = + Mirai.FileCacheStrategy.newCache(this, formatName) + + + /** + * 将图片作为单独的消息发送给指定联系人. + * + * 注意:本函数不会关闭 [ExternalResource] + * + * + * @see Contact.uploadImage 上传图片 + * @see Contact.sendMessage 最终调用, 发送消息. + */ + @JvmBlockingBridge + @JvmStatic + @JvmName("sendAsImage") + public suspend fun ExternalResource.sendAsImageTo(contact: C): MessageReceipt = + when (contact) { + is Group -> contact.uploadImage(this).sendTo(contact) + is User -> contact.uploadImage(this).sendTo(contact) + else -> error("unreachable") + } + + /** + * 上传图片并构造 [Image]. + * 这个函数可能需消耗一段时间. + * + * 注意:本函数不会关闭 [ExternalResource] + * + * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人 + * + * @see Contact.uploadImage 最终调用, 上传图片. + */ + @JvmBlockingBridge + @JvmStatic + public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = when (contact) { + is Group -> contact.uploadImage(this) + is User -> contact.uploadImage(this) + else -> error("unreachable") + } + + /** + * 将图片作为单独的消息发送给 [this] + * + * @see Contact.sendMessage 最终调用, 发送消息. + */ + @JvmSynthetic + public suspend inline fun C.sendImage(image: ExternalResource): MessageReceipt = + image.sendAsImageTo(this) + } +} + + +private fun InputStream.detectFileTypeAndClose(): String? { + val buffer = ByteArray(8) + return use { + kotlin.runCatching { it.read(buffer) }.onFailure { return null } + getFileType(buffer) + } +} + +internal class ExternalResourceImplByFileWithMd5( + private val file: RandomAccessFile, + override val md5: ByteArray, + formatName: String? +) : ExternalResource { + override val size: Long = file.length() + override val formatName: String by lazy { + formatName ?: inputStream().detectFileTypeAndClose().orEmpty() + } + + override fun inputStream(): InputStream { + check(file.filePointer == 0L) { "RandomAccessFile.inputStream cannot be opened simultaneously." } + return file.inputStream() + } + + override fun close() { + file.close() + } +} + +internal class ExternalResourceImplByFile( + private val file: RandomAccessFile, + formatName: String?, + private val closeOriginalFileOnClose: Boolean = true +) : ExternalResource { + override val size: Long = file.length() + override val md5: ByteArray by lazy { inputStream().md5() } + override val formatName: String by lazy { + formatName ?: inputStream().detectFileTypeAndClose().orEmpty() + } + + override fun inputStream(): InputStream { + check(file.filePointer == 0L) { "RandomAccessFile.inputStream cannot be opened simultaneously." } + return file.inputStream() + } + + override fun close() { + if (closeOriginalFileOnClose) file.close() + } +} + +internal class ExternalResourceImplByByteArray( + private val data: ByteArray, + formatName: String? +) : ExternalResource { + override val size: Long = data.size.toLong() + override val md5: ByteArray by lazy { data.md5() } + override val formatName: String by lazy { + formatName ?: getFileType(data.copyOf(8)).orEmpty() + } + + override fun inputStream(): InputStream = data.inputStream() + override fun close() {} +} + +private fun RandomAccessFile.inputStream(): InputStream { + val file = this + return object : InputStream() { + override fun read(): Int = file.read() + override fun read(b: ByteArray, off: Int, len: Int): Int = file.read(b, off, len) + override fun close() { + file.seek(0) + } + // don't close file on stream.close. stream may be obtained at multiple times. + }.buffered() +} + + +/* + * ImgType: + * JPG: 1000 + * PNG: 1001 + * WEBP: 1002 + * BMP: 1005 + * GIG: 2000 // gig? gif? + * APNG: 2001 + * SHARPP: 1004 + */ diff --git a/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt b/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt index 23e71fc3e..024f5c920 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt @@ -9,108 +9,57 @@ package net.mamoe.mirai.utils -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 kotlinx.coroutines.Dispatchers +import net.mamoe.mirai.IMirai +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import java.io.File +import java.io.IOException 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 /** - * 缓存策略. + * 资源缓存策略. * - * 图片上传时默认使用文件缓存. + * 由于上传资源时服务器要求提前给出 MD5 和文件大小等数据, 一些资源如 [InputStream] 需要首先缓存才能使用. * - * @see BotConfiguration.fileCacheStrategy 为 [Bot] 指定缓存策略 + * Mirai 全局都使用 [IMirai.FileCacheStrategy]. + * + * ### 使用 [FileCacheStrategy] 的操作 + * [ExternalResource.toExternalResource], + * [InputStream.uploadAsImage], + * [InputStream.sendAsImageTo] + * + * @see ExternalResource */ -@MiraiExperimentalApi public interface FileCacheStrategy { /** - * 将 [input] 缓存为 [ExternalImage]. - * 此函数应 close 这个 [Input] + * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. + * + * 注意: + * - 此函数不会关闭输入 + * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). + * + * @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai" */ - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - public fun newImageCache(input: Input): ExternalImage + @Throws(IOException::class) + public fun newCache(input: InputStream, formatName: String? = null): ExternalResource /** - * 将 [input] 缓存为 [ExternalImage]. - * 此函数应 close 这个 [InputStream] + * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai". + * + * 注意: + * - 此函数不会关闭输入 + * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). */ - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - public fun newImageCache(input: InputStream): ExternalImage - - /** - * 将 [input] 缓存为 [ExternalImage]. - * 此 [input] 的内容应是不变的. - */ - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - public fun newImageCache(input: ByteArray): ExternalImage - - /** - * 将 [input] 缓存为 [ExternalImage]. - * 此 [input] 的内容应是不变的. - */ - @MiraiExperimentalApi - @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) + @Throws(IOException::class) + public fun newCache(input: InputStream): ExternalResource = newCache(input, null) /** * 使用内存直接存储所有图片文件. */ public object MemoryCache : FileCacheStrategy { - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - override fun newImageCache(input: Input): ExternalImage { - return newImageCache(input.readBytes()) - } - - @MiraiExperimentalApi - @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()) + @Throws(IOException::class) + override fun newCache(input: InputStream, formatName: String?): ExternalResource { + return input.readBytes().toExternalResource(formatName) } } @@ -124,87 +73,27 @@ public interface FileCacheStrategy { */ public val directory: File? = null ) : FileCacheStrategy { - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - override fun newImageCache(input: Input): ExternalImage { - return ExternalImage(File.createTempFile("tmp", null, directory).apply { + private fun createTempFile(): File { + return File.createTempFile("tmp", null, directory) + } + + @Throws(IOException::class) + override fun newCache(input: InputStream, formatName: String?): ExternalResource { + return createTempFile().apply { deleteOnExit() - input.withOut(this.outputStream()) { copyTo(it) } - }.asReusableInput(true)) - } - - @MiraiExperimentalApi - @Throws(java.io.IOException::class) - override fun newImageCache(input: InputStream): ExternalImage { - return ExternalImage(File.createTempFile("tmp", null, 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 = File.createTempFile("tmp", null, 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(File.createTempFile("tmp", null, directory).apply { - deleteOnExit() - input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) } - }.asReusableInput(true)) + outputStream().use { out -> input.copyTo(out) } + }.toExternalResource(formatName) } } -} - -@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) + public companion object { + /** + * 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai 从 [IMirai.FileCacheStrategy] 获取. + * + * @see IMirai.FileCacheStrategy + */ + @MiraiExperimentalApi + @JvmStatic + public val PlatformDefault: FileCacheStrategy = TempCache(null) } - return bytesCopied -} - -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) } } } diff --git a/mirai-core-api/src/commonMain/kotlin/utils/SendImageUtilsJvmKt.kt b/mirai-core-api/src/commonMain/kotlin/utils/SendImageUtilsJvmKt.kt new file mode 100644 index 000000000..a8afd54d6 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/SendImageUtilsJvmKt.kt @@ -0,0 +1,126 @@ +/* + * 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 + */ + +/** + * 为 Kotlin 使用者实现的发送图片的一些扩展函数. + */ + +@file:Suppress("unused") +@file:JvmMultifileClass +@file:JvmName("SendResourceUtilsJvmKt") + +package net.mamoe.mirai.utils + +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.ExternalResource.Companion.sendAsImageTo +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource +import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage +import java.io.File +import java.io.InputStream + +// region IMAGE.sendAsImageTo(Contact) + +/** + * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 + * + * 注意:本函数不会关闭流 + * + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun InputStream.sendAsImageTo(contact: C): MessageReceipt = + runBIO { + @Suppress("BlockingMethodInNonBlockingContext") + toExternalResource("png") + }.withUse { sendAsImageTo(contact) } + +/** + * 将文件作为图片发送到指定联系人 + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun File.sendAsImageTo(contact: C): MessageReceipt { + require(this.exists() && this.canRead()) + return toExternalResource("png").withUse { sendAsImageTo(contact) } +} + +// endregion + +// region IMAGE.Upload(Contact): Image + +/** + * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image] + * + * 注意:本函数不会关闭流 + * + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun InputStream.uploadAsImage(contact: Contact): Image = + @Suppress("BlockingMethodInNonBlockingContext") + runBIO { toExternalResource("png") }.withUse { uploadAsImage(contact) } + +/** + * 将文件作为图片上传后构造 [Image] + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun File.uploadAsImage(contact: Contact): Image { + require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" } + return toExternalResource("png").withUse { uploadAsImage(contact) } +} + +/** + * 将文件作为语音上传后构造 [Voice] + * + * - 请手动关闭输入流 + * - 请使用 amr 或 silk 格式 + * + * @suppress 注意,这只是个实验性功能且随时可能会删除 + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@MiraiExperimentalApi("语音支持处于实验性阶段") +public suspend inline fun InputStream.uploadAsGroupVoice(group: Group): Voice { + return group.uploadVoice(this) +} + +// endregion + +// region Contact.uploadImage(IMAGE) + +/** + * 读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送 + * + * 注意:本函数不会关闭流 + * + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun Contact.uploadImage(imageStream: InputStream): Image = + imageStream.uploadAsImage(this@uploadImage) + +/** + * 将文件作为图片上传, 但不发送 + * @throws OverFileSizeMaxException + */ +@Throws(OverFileSizeMaxException::class) +@JvmSynthetic +public suspend inline fun Contact.uploadImage(file: File): Image = file.uploadAsImage(this) + +// endregion diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/ChunkedFlowSession.kt b/mirai-core-api/src/commonMain/kotlin/utils/internal/ChunkedFlowSession.kt deleted file mode 100644 index d424200f7..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/ChunkedFlowSession.kt +++ /dev/null @@ -1,152 +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 - */ - -package net.mamoe.mirai.utils.internal - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.io.ByteReadChannel -import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.Closeable -import kotlinx.io.core.Input -import kotlinx.serialization.InternalSerializationApi -import net.mamoe.mirai.utils.MiraiExperimentalApi -import java.io.InputStream -import kotlin.jvm.JvmField - - -@MiraiExperimentalApi -public interface ChunkedFlowSession : Closeable { - public val flow: Flow - override fun close() -} - -internal inline fun ChunkedFlowSession.map(crossinline mapper: suspend ChunkedFlowSession.(T) -> R): ChunkedFlowSession { - return object : ChunkedFlowSession { - override val flow: Flow = this@map.flow.map { this@map.mapper(it) } - override fun close() = this@map.close() - } -} - - -/** - * 由 [chunkedFlow] 分割得到的区块 - */ -@MiraiExperimentalApi -public class ChunkedInput( - /** - * 区块的数据. - * 由 [ByteArrayPool] 缓存并管理, 只可在 [Flow.collect] 中访问. - * 它的大小由 [ByteArrayPool.BUFFER_SIZE] 决定, 而有效(有数据)的大小由 [bufferSize] 决定. - * - * **注意**: 不要将他带出 [Flow.collect] 作用域, 否则将造成内存泄露 - */ - @JvmField public val buffer: ByteArray, - @JvmField internal var size: Int -) { - /** - * [buffer] 的有效大小 - */ - public val bufferSize: Int get() = size -} - -/** - * 创建将 [ByteReadPacket] 以固定大小分割的 [Sequence]. - * - * 对于一个 1000 长度的 [ByteReadPacket] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence], - * 其长度分别为: 300, 300, 300, 100. - * - * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence] - */ -internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow { - ByteArrayPool.checkBufferSize(sizePerPacket) - if (this.remaining <= sizePerPacket.toLong()) { - return flowOf( - ChunkedInput( - buffer, - this.readAvailable(buffer, 0, sizePerPacket) - ) - ) - } - return flow { - val chunkedInput = ChunkedInput(buffer, 0) - do { - chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) - emit(chunkedInput) - } while (this@chunkedFlow.isNotEmpty) - } -} - -/** - * 创建将 [ByteReadChannel] 以固定大小分割的 [Sequence]. - * - * 对于一个 1000 长度的 [ByteReadChannel] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence], - * 其长度分别为: 300, 300, 300, 100. - */ -internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow { - ByteArrayPool.checkBufferSize(sizePerPacket) - if (this.isClosedForRead) { - return flowOf() - } - return flow { - val chunkedInput = ChunkedInput(buffer, 0) - do { - chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) - emit(chunkedInput) - } while (!this@chunkedFlow.isClosedForRead) - } -} - - -/** - * 创建将 [Input] 以固定大小分割的 [Sequence]. - * - * 对于一个 1000 长度的 [Input] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence], - * 其长度分别为: 300, 300, 300, 100. - */ -@OptIn(ExperimentalCoroutinesApi::class) -internal fun Input.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow { - ByteArrayPool.checkBufferSize(sizePerPacket) - - if (this.endOfInput) { - return flowOf() - } - - return flow { - val chunkedInput = ChunkedInput(buffer, 0) - while (!this@chunkedFlow.endOfInput) { - chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) - emit(chunkedInput) - } - } -} - -/** - * 创建将 [ByteReadPacket] 以固定大小分割的 [Sequence]. - * - * 对于一个 1000 长度的 [ByteReadPacket] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence], - * 其长度分别为: 300, 300, 300, 100. - * - * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence] - */ -@OptIn(ExperimentalCoroutinesApi::class, InternalSerializationApi::class) -internal fun InputStream.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow { - require(sizePerPacket <= buffer.size) { "sizePerPacket is too large. Maximum buffer size=buffer.size=${buffer.size}" } - - return flow { - val chunkedInput = ChunkedInput(buffer, 0) - while (this@chunkedFlow.available() != 0) { - chunkedInput.size = this@chunkedFlow.read(buffer, 0, sizePerPacket) - emit(chunkedInput) - } - } -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.jvm.kt b/mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.jvm.kt deleted file mode 100644 index 17079eda8..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.jvm.kt +++ /dev/null @@ -1,65 +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 - */ - -package net.mamoe.mirai.utils.internal - -import io.ktor.utils.io.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.io.core.Input -import net.mamoe.mirai.utils.FileCacheStrategy -import java.awt.image.BufferedImage -import java.io.InputStream -import java.net.URL - -internal actual class DeferredReusableInput actual constructor( - val input: Any, - val extraArg: Any? -) : ReusableInput { - - - 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 URL -> strategy.newImageCache(input) - is BufferedImage -> strategy.newImageCache(input, extraArg as String) - 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") - } - - override fun asInput(): Input { - return delegate?.asInput() ?: error("DeferredReusableInput not yet initialized") - } - - override fun release() { - return delegate?.release() ?: error("DeferredReusableInput not yet initialized") - } - - actual val initialized: Boolean get() = delegate != null -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/ReusableInput.kt b/mirai-core-api/src/commonMain/kotlin/utils/internal/ReusableInput.kt deleted file mode 100644 index b772de789..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/ReusableInput.kt +++ /dev/null @@ -1,28 +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 - */ - -package net.mamoe.mirai.utils.internal - -import io.ktor.utils.io.* -import kotlinx.io.core.Input - -internal interface ReusableInput { - val md5: ByteArray - val size: Long - - fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession - suspend fun writeTo(out: ByteWriteChannel): Long - - /** - * Remember to close. - */ - fun asInput(): Input - - fun release() -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/asReusableInput.kt b/mirai-core-api/src/commonMain/kotlin/utils/internal/asReusableInput.kt deleted file mode 100644 index f656cd758..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/asReusableInput.kt +++ /dev/null @@ -1,145 +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 - */ - -package net.mamoe.mirai.utils.internal - -import io.ktor.utils.io.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.withContext -import kotlinx.io.core.Input -import kotlinx.io.streams.asInput -import net.mamoe.mirai.message.data.toLongUnsigned -import java.io.ByteArrayInputStream -import java.io.File -import java.io.InputStream - -internal fun asReusableInput0(input: ByteArray): ReusableInput = input.asReusableInput() - -internal const val DEFAULT_REUSABLE_INPUT_BUFFER_SIZE = 8192 - -internal fun ByteArray.asReusableInput(): ReusableInput { - return object : ReusableInput { - override val md5: ByteArray = md5() - override val size: Long get() = this@asReusableInput.size.toLongUnsigned() - - override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession { - return object : ChunkedFlowSession { - private val stream = inputStream() - override val flow: Flow = stream.chunkedFlow( - sizePerPacket, - ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)) - ) - - override fun close() { - stream.close() - // nothing to do - } - } - } - - override suspend fun writeTo(out: ByteWriteChannel): Long { - out.writeFully(this@asReusableInput, 0, this@asReusableInput.size) - out.flush() - return this@asReusableInput.size.toLongUnsigned() - } - - override fun asInput(): Input { - return ByteArrayInputStream(this@asReusableInput).asInput() - } - - override fun release() { - // nothing to do - } - } -} - -internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput { - return object : ReusableInput { - override val md5: ByteArray = inputStream().use { it.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, - ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)) - ) - - override fun close() { - stream.close() - } - } - } - - override suspend fun writeTo(out: ByteWriteChannel): Long { - return inputStream().use { it.copyTo(out) } - } - - override fun asInput(): Input { - return inputStream().asInput() - } - - override fun release() { - if (deleteOnClose) this@asReusableInput.delete() - } - - } -} - -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, - ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)) - ) - - override fun close() { - stream.close() - } - } - } - - override suspend fun writeTo(out: ByteWriteChannel): Long { - return inputStream().use { it.copyTo(out) } - } - - override fun asInput(): Input { - return inputStream().asInput() - } - - override fun release() { - if (deleteOnClose) this@asReusableInput.delete() - } - } -} - -private suspend fun InputStream.copyTo(out: ByteWriteChannel): Long = withContext(Dispatchers.IO) { - var bytesCopied: Long = 0 - - ByteArrayPool.useInstance { buffer -> - var bytes = read(buffer) - while (bytes >= 0) { - out.writeFully(buffer, 0, bytes) - bytesCopied += bytes - bytes = read(buffer) - } - } - - out.flush() - - return@withContext bytesCopied -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/md5.jvm.kt b/mirai-core-api/src/commonMain/kotlin/utils/internal/md5.jvm.kt deleted file mode 100644 index 902a98d05..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/md5.jvm.kt +++ /dev/null @@ -1,29 +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.internal - -import java.io.InputStream -import java.security.MessageDigest - -internal actual fun ByteArray.md5(offset: Int, length: Int): ByteArray { - this.checkOffsetAndLength(offset, length) - return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest() -} - -internal actual fun InputStream.md5(): ByteArray { - val digest = MessageDigest.getInstance("md5") - digest.reset() - this.readInSequence { buf, len -> - digest.update(buf, 0, len) - } - return digest.digest() -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/sendTo.kt b/mirai-core-api/src/commonMain/kotlin/utils/sendTo.kt deleted file mode 100644 index 87194d702..000000000 --- a/mirai-core-api/src/commonMain/kotlin/utils/sendTo.kt +++ /dev/null @@ -1,156 +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") -@file:JvmMultifileClass -@file:JvmName("SendImageUtilsJvmKt") - -package net.mamoe.mirai.utils - -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 java.awt.image.BufferedImage -import java.io.File -import java.io.InputStream - -// region IMAGE.sendAsImageTo(Contact) - -/** - * 在 [Dispatchers.IO] 中将图片发送到指定联系人. 不会创建临时文件 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend fun BufferedImage.sendTo(contact: C): MessageReceipt = - toExternalImage().sendTo(contact) - -/** - * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend fun InputStream.sendAsImageTo(contact: C): MessageReceipt = - toExternalImage().sendTo(contact) - -/** - * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend fun File.sendAsImageTo(contact: C): MessageReceipt { - require(this.exists() && this.canRead()) - return toExternalImage().sendTo(contact) -} - -// endregion - -// region IMAGE.Upload(Contact): Image - -/** - * 在 [Dispatchers.IO] 中将图片上传后构造 [Image]. 不会创建临时文件 - * @throws OverFileSizeMaxException - */ -@JvmSynthetic -@Throws(OverFileSizeMaxException::class) -public suspend fun BufferedImage.upload(contact: Contact): Image = - toExternalImage().upload(contact) - -/** - * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image] - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend fun InputStream.uploadAsImage(contact: Contact): Image = - toExternalImage().upload(contact) - -/** - * 在 [Dispatchers.IO] 中将文件作为图片上传后构造 [Image] - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend fun File.uploadAsImage(contact: Contact): Image { - require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" } - return toExternalImage().upload(contact) -} - -/** - * 在 [Dispatchers.IO] 中将文件作为语音上传后构造 [Voice] - * - * - 请手动关闭输入流 - * - 请使用 amr 或 silk 格式 - * - * @suppress 注意,这只是个实验性功能且随时可能会删除 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -@MiraiExperimentalApi("语音支持处于实验性阶段") -public suspend fun InputStream.uploadAsGroupVoice(group: Group): Voice { - return group.uploadVoice(this) -} - -// endregion - -// region Contact.sendImage(IMAGE) - -/** - * 在 [Dispatchers.IO] 中将图片发送到指定联系人. 不会保存临时文件 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun C.sendImage(bufferedImage: BufferedImage): MessageReceipt = - bufferedImage.sendTo(this) - -/** - * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun C.sendImage(imageStream: InputStream): MessageReceipt = - imageStream.sendAsImageTo(this) - -/** - * 在 [Dispatchers.IO] 中将文件作为图片发送到指定联系人 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun C.sendImage(file: File): MessageReceipt = file.sendAsImageTo(this) - -// endregion - -// region Contact.uploadImage(IMAGE) - -/** - * 在 [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun Contact.uploadImage(bufferedImage: BufferedImage): Image = bufferedImage.upload(this) - -/** - * 在 [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun Contact.uploadImage(imageStream: InputStream): Image = imageStream.uploadAsImage(this) - -/** - * 在 [Dispatchers.IO] 中将文件作为图片上传, 但不发送 - * @throws OverFileSizeMaxException - */ -@Throws(OverFileSizeMaxException::class) -public suspend inline fun Contact.uploadImage(file: File): Image = file.uploadAsImage(this) - -// endregion \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/md5.common.kt b/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt similarity index 51% rename from mirai-core-api/src/commonMain/kotlin/utils/internal/md5.common.kt rename to mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt index 2e8a832bf..46dfc07d7 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/md5.common.kt +++ b/mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt @@ -7,60 +7,38 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("EXPERIMENTAL_API_USAGE", "unused") +@file:JvmMultifileClass +@file:JvmName("MiraiUtils") -package net.mamoe.mirai.utils.internal +package net.mamoe.mirai.utils import kotlinx.io.pool.DefaultPool -import kotlinx.io.pool.ObjectPool -import java.io.InputStream - -internal expect fun InputStream.md5(): ByteArray -internal expect fun ByteArray.md5(offset: Int = 0, length: Int = this.size - offset): ByteArray - -@Suppress("DuplicatedCode") // false positive. `this` is not the same for `List` and `ByteArray` -internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) { - require(offset >= 0) { "offset shouldn't be negative: $offset" } - require(length >= 0) { "length shouldn't be negative: $length" } - require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } -} - - -internal inline fun InputStream.readInSequence(block: (ByteArray, len: Int) -> Unit) { - var read: Int - ByteArrayPool.useInstance { buf -> - while (this.read(buf).also { read = it } != -1) { - block(buf, read) - } - } -} - /** * 缓存 [ByteArray] 实例的 [ObjectPool] */ -internal object ByteArrayPool : DefaultPool(256) { +public object ByteArrayPool : DefaultPool(256) { /** * 每一个 [ByteArray] 的大小 */ - const val BUFFER_SIZE: Int = 8192 * 8 + public const val BUFFER_SIZE: Int = 8192 * 8 override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE) override fun clearInstance(instance: ByteArray): ByteArray = instance - fun checkBufferSize(size: Int) { + public fun checkBufferSize(size: Int) { require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" } } - fun checkBufferSize(size: Long) { + public fun checkBufferSize(size: Long) { require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" } } /** * 请求一个大小至少为 [requestedSize] 的 [ByteArray] 实例. */ // 不要写为扩展函数. 它需要优先于 kotlinx.io 的扩展函数 resolve - inline fun useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R { + public inline fun useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R { if (requestedSize > BUFFER_SIZE) { return ByteArray(requestedSize).run(block) } diff --git a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt new file mode 100644 index 000000000..190750f01 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt @@ -0,0 +1,68 @@ +/* + * 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("MiraiUtils") + +package net.mamoe.mirai.utils + + +@JvmOverloads +public fun generateImageId(md5: ByteArray, format: String = "mirai"): String { + return """{${generateUUID(md5)}}.$format""" +} + +public fun generateUUID(md5: ByteArray): String { + return "${md5[0, 3]}-${md5[4, 5]}-${md5[6, 7]}-${md5[8, 9]}-${md5[10, 15]}" +} + +@JvmSynthetic +internal operator fun ByteArray.get(rangeStart: Int, rangeEnd: Int): String = buildString { + for (it in rangeStart..rangeEnd) { + append(this@get[it].fixToString()) + } +} + +private fun Byte.fixToString(): String { + return when (val b = this.toInt() and 0xff) { + in 0..15 -> "0${this.toString(16).toUpperCase()}" + else -> b.toString(16).toUpperCase() + } +} + +@OptIn(ExperimentalUnsignedTypes::class) +@JvmOverloads +@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray +public fun ByteArray.toUHexString( + separator: String = " ", + offset: Int = 0, + length: Int = this.size - offset +): String { + this.checkOffsetAndLength(offset, length) + if (length == 0) { + return "" + } + val lastIndex = offset + length + return buildString(length * 2) { + this@toUHexString.forEachIndexed { index, it -> + if (index in offset until lastIndex) { + var ret = it.toUByte().toString(16).toUpperCase() + if (ret.length == 1) ret = "0$ret" + append(ret) + if (index < lastIndex - 1) append(separator) + } + } + } +} + +public fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) { + require(offset >= 0) { "offset shouldn't be negative: $offset" } + require(length >= 0) { "length shouldn't be negative: $length" } + require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.common.kt b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt similarity index 53% rename from mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.common.kt rename to mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt index 044811c41..4d40a9cac 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/internal/DeferredReusableInput.common.kt +++ b/mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt @@ -7,14 +7,15 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.utils.internal +@file:JvmMultifileClass +@file:JvmName("MiraiUtils") -import net.mamoe.mirai.utils.FileCacheStrategy +package net.mamoe.mirai.utils -internal expect class DeferredReusableInput(input: Any, extraArg: Any?) : ReusableInput { - val initialized: Boolean +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext - - suspend fun init(strategy: FileCacheStrategy) - -} \ No newline at end of file +public suspend inline fun runBIO( + noinline block: suspend CoroutineScope.() -> R +): R = withContext(Dispatchers.IO, block) \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/Files.kt b/mirai-core-utils/src/commonMain/kotlin/Files.kt new file mode 100644 index 000000000..26fe6abb5 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/Files.kt @@ -0,0 +1,46 @@ +/* + * 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("MiraiUtils") + +package net.mamoe.mirai.utils + +/** + * 文件头和文件类型列表 + */ +public val FILE_TYPES: MutableMap = mutableMapOf( + "FFD8FF" to "jpg", + "89504E47" to "png", + "47494638" to "gif", + "49492A00" to "tif", + "424D" to "bmp", + "57415645" to "wav", +) + +/* + + startsWith("FFD8") -> "jpg" + startsWith("89504E47") -> "png" + startsWith("47494638") -> "gif" + startsWith("424D") -> "bmp" + */ + +/** + * 根据文件头获取文件类型 + */ +public fun getFileType(fileHeader: ByteArray): String? { + val hex = fileHeader.toUHexString("") + FILE_TYPES.forEach { (k, v) -> + if (hex.startsWith(k)) { + return v + } + } + return null +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt b/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt new file mode 100644 index 000000000..72c04f866 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt @@ -0,0 +1,146 @@ +/* + * 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("MiraiUtils") + +package net.mamoe.mirai.utils + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import kotlinx.io.core.Input +import kotlinx.io.core.readAvailable +import java.io.* +import java.net.Inet4Address +import java.security.MessageDigest +import java.util.zip.Deflater +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import java.util.zip.Inflater +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +public object MiraiPlatformUtils { + /** + * Ktor HttpClient. 不同平台使用不同引擎. + */ + public val Http: HttpClient = HttpClient(CIO) +} + +@JvmOverloads +public fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray { + checkOffsetAndLength(offset, length) + if (length == 0) return ByteArray(0) + + val inflater = Inflater() + inflater.reset() + ByteArrayOutputStream().use { output -> + inflater.setInput(this, offset, length) + ByteArray(DEFAULT_BUFFER_SIZE).let { + while (!inflater.finished()) { + output.write(it, 0, inflater.inflate(it)) + } + } + + inflater.end() + return output.toByteArray() + } +} + +public fun InputStream.md5(): ByteArray { + val digest = MessageDigest.getInstance("md5") + digest.reset() + use { input -> + object : OutputStream() { + override fun write(b: Int) { + digest.update(b.toByte()) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + digest.update(b, off, len) + } + }.use { output -> + input.copyTo(output) + } + } + return digest.digest() +} + +/** + * Localhost 解析 + */ +public fun localIpAddress(): String = runCatching { + Inet4Address.getLocalHost().hostAddress +}.getOrElse { "192.168.1.123" } + +public fun String.md5(): ByteArray = toByteArray().md5() + +@JvmOverloads +public fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray { + checkOffsetAndLength(offset, length) + return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest() +} + +@JvmOverloads +public fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray { + return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() } +} + +@JvmOverloads +public fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray { + ByteArrayOutputStream().use { buf -> + GZIPOutputStream(buf).use { gzip -> + inputStream(offset, length).use { t -> t.copyTo(gzip) } + } + buf.flush() + return buf.toByteArray() + } +} + +@JvmOverloads +public fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray { + checkOffsetAndLength(offset, length) + if (length == 0) return ByteArray(0) + + val deflater = Deflater() + deflater.setInput(this, offset, length) + deflater.finish() + + ByteArray(DEFAULT_BUFFER_SIZE).let { + return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() } + } +} + +public inline fun C.withUse(block: C.() -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return use(block) +} + +@Throws(IOException::class) +@JvmOverloads +public 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 +} + +public 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) } } +} diff --git a/mirai-core-api/src/commonTest/kotlin/utils/ExternalImageTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt similarity index 99% rename from mirai-core-api/src/commonTest/kotlin/utils/ExternalImageTest.kt rename to mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt index 522d03fb3..1fa6b1ba0 100644 --- a/mirai-core-api/src/commonTest/kotlin/utils/ExternalImageTest.kt +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ExternalImageTest.kt @@ -6,6 +6,7 @@ * * https://github.com/mamoe/mirai/blob/master/LICENSE */ + package net.mamoe.mirai.utils import kotlin.test.Test diff --git a/mirai-core/src/commonMain/kotlin/BotAccount.kt b/mirai-core/src/commonMain/kotlin/BotAccount.kt index d5361cce3..d3dacef20 100644 --- a/mirai-core/src/commonMain/kotlin/BotAccount.kt +++ b/mirai-core/src/commonMain/kotlin/BotAccount.kt @@ -10,10 +10,8 @@ package net.mamoe.mirai.internal -import kotlinx.io.core.toByteArray -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.MiraiExperimentalApi -import kotlin.jvm.JvmSynthetic +import net.mamoe.mirai.utils.md5 internal data class BotAccount( @JvmSynthetic @@ -22,7 +20,7 @@ internal data class BotAccount( @MiraiExperimentalApi val passwordMd5: ByteArray // md5 ) { - constructor(id: Long, passwordPlainText: String) : this(id, MiraiPlatformUtils.md5(passwordPlainText.toByteArray())) + constructor(id: Long, passwordPlainText: String) : this(id, passwordPlainText.md5()) override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index bbeb8c2ea..1d73f5cf3 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -30,7 +30,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils.encodeToString import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt @@ -39,10 +38,8 @@ import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1 import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2 import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX -import net.mamoe.mirai.utils.BotConfiguration -import net.mamoe.mirai.utils.MiraiExperimentalApi -import net.mamoe.mirai.utils.cast -import net.mamoe.mirai.utils.currentTimeSeconds +import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.absoluteValue import kotlin.random.Random @@ -62,6 +59,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { override val BotFactory: BotFactory get() = BotFactoryImpl + override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault + @OptIn(LowLevelApi::class) override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) { @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @@ -609,8 +608,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { bot, response.proto.uint32UpIp.zip(response.proto.uint32UpPort), response.proto.msgSig, - MiraiPlatformUtils.md5(body), - net.mamoe.mirai.utils.internal.asReusableInput0(body), // don't use toLongUnsigned: Overload resolution ambiguity + body.md5(), + body.toExternalResource(null), // don't use toLongUnsigned: Overload resolution ambiguity "group long message", 27 ) @@ -801,7 +800,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { else -> error("Internal error: unsupported image class: ${image::class.simpleName}") } - @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean { if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) { throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE") diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt index 4bcc92ef7..41b7bd4c3 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt @@ -15,14 +15,15 @@ import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.BeforeImageUploadEvent import net.mamoe.mirai.event.events.EventCancelledException import net.mamoe.mirai.event.events.ImageUploadEvent +import net.mamoe.mirai.internal.message.OfflineFriendImage import net.mamoe.mirai.internal.network.highway.postImage import net.mamoe.mirai.internal.network.highway.sizeToString import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.verbose import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt @@ -38,11 +39,8 @@ internal abstract class AbstractUser( final override val remark: String = friendInfo.remark @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - override suspend fun uploadImage(image: ExternalImage): Image = try { - if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { - image.input.init(bot.configuration.fileCacheStrategy) - } - if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) { + override suspend fun uploadImage(resource: ExternalResource): Image { + if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) { throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") } val response = bot.network.run { @@ -51,22 +49,22 @@ internal abstract class AbstractUser( srcUin = bot.id.toInt(), dstUin = id.toInt(), fileId = 0, - fileMd5 = image.md5, - fileSize = image.input.size.toInt(), - fileName = image.md5.toUHexString("") + "." + image.formatName, + fileMd5 = resource.md5, + fileSize = resource.size.toInt(), + fileName = resource.md5.toUHexString("") + "." + resource.formatName, imgOriginal = 1 ) ).sendAndExpect() } - when (response) { - is LongConn.OffPicUp.Response.FileExists -> net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId) + return when (response) { + is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId) .also { - ImageUploadEvent.Succeed(this, image, it).broadcast() + ImageUploadEvent.Succeed(this, resource, it).broadcast() } is LongConn.OffPicUp.Response.RequireUpload -> { bot.network.logger.verbose { - "[Http] Uploading friend image, size=${image.input.size.sizeToString()}" + "[Http] Uploading friend image, size=${resource.size.sizeToString()}" } val time = measureTime { @@ -74,13 +72,13 @@ internal abstract class AbstractUser( "0x6ff0070", bot.id, null, - imageInput = image.input, + imageInput = resource, uKeyHex = response.uKey.toUHexString("") ) } bot.network.logger.verbose { - "[Http] Uploading friend image: succeed at ${(image.input.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s" + "[Http] Uploading friend image: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s" } /* @@ -94,16 +92,14 @@ internal abstract class AbstractUser( )*/ // 为什么不能 ?? - net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId).also { - ImageUploadEvent.Succeed(this, image, it).broadcast() + OfflineFriendImage(response.resourceId).also { + ImageUploadEvent.Succeed(this, resource, it).broadcast() } } is LongConn.OffPicUp.Response.Failed -> { - ImageUploadEvent.Failed(this, image, -1, response.message).broadcast() + ImageUploadEvent.Failed(this, resource, -1, response.message).broadcast() error(response.message) } } - } finally { - image.input.release() } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt b/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt index 9c4db6c2e..63ac8f3f0 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt @@ -18,7 +18,7 @@ import net.mamoe.mirai.internal.MiraiImpl import net.mamoe.mirai.message.action.MemberNudge import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.MemberDeprecatedApi import kotlin.coroutines.CoroutineContext @@ -39,7 +39,7 @@ internal class AnonymousMemberImpl( override val remark: String get() = memberInfo.remark override fun nudge(): MemberNudge = notSupported("Nudge") - override suspend fun uploadImage(image: ExternalImage): Image = notSupported("Upload image to") + override suspend fun uploadImage(resource: ExternalResource): Image = notSupported("Upload image to") override suspend fun unmute() { } diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index e1ba52f65..7fd3dfd54 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -24,6 +24,7 @@ import net.mamoe.mirai.internal.message.MessageSourceToGroupImpl import net.mamoe.mirai.internal.message.OfflineGroupImage import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable import net.mamoe.mirai.internal.message.firstIsInstanceOrNull +import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.internal.network.highway.HighwayHelper import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg @@ -31,9 +32,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGro import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils.estimateLength -import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* @@ -228,53 +227,47 @@ internal class GroupImpl( return result.getOrThrow() } - @Suppress("DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @OptIn(ExperimentalTime::class) - override suspend fun uploadImage(image: ExternalImage): Image = try { - if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { - image.input.init(bot.configuration.fileCacheStrategy) - } - if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) { + override suspend fun uploadImage(resource: ExternalResource): Image { + if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) { throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") } - bot.network.run { + bot.network.run { val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp( bot.client, uin = bot.id, groupCode = id, - md5 = image.md5, - size = image.input.size.toInt() + md5 = resource.md5, + size = resource.size.toInt() ).sendAndExpect() @Suppress("UNCHECKED_CAST") // bug when (response) { is ImgStore.GroupPicUp.Response.Failed -> { - ImageUploadEvent.Failed(this@GroupImpl, image, response.resultCode, response.message).broadcast() + ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast() if (response.message == "over file size max") throw OverFileSizeMaxException() error("upload group image failed with reason ${response.message}") } is ImgStore.GroupPicUp.Response.FileExists -> { - val resourceId = image.calculateImageResourceId() + val resourceId = resource.calculateResourceId() return OfflineGroupImage(imageId = resourceId) - .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() } + .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() } } is ImgStore.GroupPicUp.Response.RequireUpload -> { HighwayHelper.uploadImageToServers( bot, response.uploadIpList.zip(response.uploadPortList), response.uKey, - image.input, + resource, kind = "group image", commandId = 2 ) - val resourceId = image.calculateImageResourceId() + val resourceId = resource.calculateResourceId() return OfflineGroupImage(imageId = resourceId) - .also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() } + .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() } } } } - } finally { - image.input.release() } /** @@ -290,7 +283,7 @@ internal class GroupImpl( if (content.size > 1048576) { throw OverFileSizeMaxException() } - val md5 = MiraiPlatformUtils.md5(content) + val md5 = content.md5() val codec = with(content.copyOfRange(0, 10).toUHexString("")) { when { startsWith("2321414D52") -> 0 // amr diff --git a/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt b/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt index 88d2c7d29..97f600413 100644 --- a/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/OtherClientImpl.kt @@ -15,7 +15,7 @@ import net.mamoe.mirai.contact.OtherClientInfo import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.ExternalResource import kotlin.coroutines.CoroutineContext internal class OtherClientImpl( @@ -27,7 +27,7 @@ internal class OtherClientImpl( throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.") } - override suspend fun uploadImage(image: ExternalImage): Image { + override suspend fun uploadImage(resource: ExternalResource): Image { throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.") } diff --git a/mirai-core/src/commonMain/kotlin/message/conversions.kt b/mirai-core/src/commonMain/kotlin/message/conversions.kt index 7d6440171..d93e87bb2 100644 --- a/mirai-core/src/commonMain/kotlin/message/conversions.kt +++ b/mirai-core/src/commonMain/kotlin/message/conversions.kt @@ -28,6 +28,8 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.safeCast +import net.mamoe.mirai.utils.unzip +import net.mamoe.mirai.utils.zip import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -58,7 +60,7 @@ internal fun MessageChain.toRichTextElems( fun transformOneMessage(it: Message) { if (it is RichMessage) { - val content = MiraiPlatformUtils.zip(it.content.toByteArray()) + val content = it.content.toByteArray().zip() when (it) { is ForwardMessageInternal -> { elements.add( @@ -422,7 +424,7 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, botId: { "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) { when (element.lightApp.data[0].toInt()) { 0 -> element.lightApp.data.encodeToString(offset = 1) - 1 -> MiraiPlatformUtils.unzip(element.lightApp.data, 1).encodeToString() + 1 -> element.lightApp.data.unzip(1).encodeToString() else -> error("unknown compression flag=${element.lightApp.data[0]}") } } @@ -432,7 +434,7 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, botId: val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) { when (element.richMsg.template1[0].toInt()) { 0 -> element.richMsg.template1.encodeToString(offset = 1) - 1 -> MiraiPlatformUtils.unzip(element.richMsg.template1, 1).encodeToString() + 1 -> element.richMsg.template1.unzip(1).encodeToString() else -> error("unknown compression flag=${element.richMsg.template1[0]}") } } diff --git a/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt b/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt index bd1c34530..0845e8f75 100644 --- a/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/imagesImpl.kt @@ -25,19 +25,31 @@ import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1 import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2 import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX import net.mamoe.mirai.message.data.md5 -import net.mamoe.mirai.utils.ExternalImage +import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.generateImageId + +/* + * ImgType: + * JPG: 1000 + * PNG: 1001 + * WEBP: 1002 + * BMP: 1005 + * GIG: 2000 // gig? gif? + * APNG: 2001 + * SHARPP: 1004 + */ internal class OnlineGroupImageImpl( internal val delegate: ImMsgBody.CustomFace ) : @Suppress("DEPRECATION") OnlineGroupImage() { - override val imageId: String = ExternalImage.generateImageId( + override val imageId: String = generateImageId( delegate.md5, delegate.filePath.substringAfterLast('.') ).takeIf { GROUP_IMAGE_ID_REGEX.matches(it) - } ?: ExternalImage.generateImageId(delegate.md5) + } ?: generateImageId(delegate.md5) override val originUrl: String get() = if (delegate.origUrl.isBlank()) { @@ -145,14 +157,12 @@ internal interface SuspendDeferredOriginUrlAware : Image { @MiraiExperimentalApi("Will be renamed to OfflineImage on 1.2.0") @Suppress("DEPRECATION_ERROR") internal class ExperimentalDeferredImage internal constructor( - @Suppress("CanBeParameter") private val externalImage: ExternalImage // for future use + @Suppress("CanBeParameter") private val externalImage: ExternalResource // for future use ) : AbstractImage(), SuspendDeferredOriginUrlAware { override suspend fun getUrl(bot: Bot): String { TODO() } - - @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - override val imageId: String = externalImage.calculateImageResourceId() + override val imageId: String = externalImage.calculateResourceId() } @Suppress("EXPOSED_SUPER_INTERFACE") diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index c0017970d..ba0f3adfe 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -38,7 +38,7 @@ internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddre */ @Suppress("RemoveRedundantQualifierName") // bug private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray = - net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(androidId + macAddress) + (androidId + macAddress).md5() /** * 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray] @@ -317,7 +317,7 @@ internal open class QQAndroidClient( @Suppress("RemoveRedundantQualifierName") // bug internal fun generateTgtgtKey(guid: ByteArray): ByteArray = - net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(getRandomByteArray(16) + guid) + (getRandomByteArray(16) + guid).md5() internal class ReserveUinInfo( diff --git a/mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt b/mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt index ab4d38ea7..2f8e7c43b 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/HighwayHelper.kt @@ -7,8 +7,6 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - package net.mamoe.mirai.internal.network.highway import io.ktor.client.* @@ -16,21 +14,25 @@ import io.ktor.client.request.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.utils.io.* +import io.ktor.utils.io.jvm.javaio.* import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.isActive import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.io.core.discardExact -import kotlinx.io.core.use +import kotlinx.io.core.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead -import net.mamoe.mirai.internal.utils.* +import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.utils.PlatformSocket +import net.mamoe.mirai.internal.utils.SocketException +import net.mamoe.mirai.internal.utils.addSuppressedMirai import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.withUse -import net.mamoe.mirai.utils.internal.ReusableInput -import net.mamoe.mirai.utils.verbose +import net.mamoe.mirai.internal.utils.toIpV4AddressString +import net.mamoe.mirai.utils.* +import java.io.InputStream import kotlin.math.roundToInt import kotlin.time.ExperimentalTime import kotlin.time.measureTime @@ -41,7 +43,7 @@ internal suspend fun HttpClient.postImage( htcmd: String, uin: Long, groupcode: Long?, - imageInput: ReusableInput, + imageInput: ExternalResource, uKeyHex: String ): Boolean = post { url { @@ -65,12 +67,10 @@ internal suspend fun HttpClient.postImage( body = object : OutgoingContent.WriteChannelContent() { override val contentType: ContentType = ContentType.Image.Any - override val contentLength: Long = imageInput.size - + override val contentLength: Long = imageInput.size.toLong() override suspend fun writeTo(channel: ByteWriteChannel) { - imageInput.writeTo(channel) - + imageInput.inputStream().withUse { copyTo(channel) } } } } == HttpStatusCode.OK @@ -82,7 +82,7 @@ internal object HighwayHelper { bot: QQAndroidBot, servers: List>, uKey: ByteArray, - image: ReusableInput, + image: ExternalResource, kind: String, commandId: Int ) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId) @@ -94,11 +94,11 @@ internal object HighwayHelper { servers: List>, uKey: ByteArray, md5: ByteArray, - input: ReusableInput, + input: ExternalResource, kind: String, commandId: Int ) = servers.retryWithServers( - (input.size * 1000 / 1024 / 10).coerceAtLeast(5000), + (input.size * 1000 / 1024 / 10).coerceAtLeast(5000).toLong(), onFail = { throw IllegalStateException("cannot upload $kind, failed on all servers.", it) } @@ -131,7 +131,7 @@ internal object HighwayHelper { serverIp: String, serverPort: Int, ticket: ByteArray, - imageInput: ReusableInput, + imageInput: ExternalResource, fileMd5: ByteArray, commandId: Int // group=2, friend=1 ) { @@ -157,18 +157,16 @@ internal object HighwayHelper { ticket = ticket, data = imageInput, fileMd5 = fileMd5 - ).withUse { - flow.collect { - socket.send(it) - //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00 + ).useAll { + socket.send(it) + //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00 - socket.read().withUse { - discardExact(1) - val headLength = readInt() - discardExact(4) - val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength) - check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" } - } + socket.read().withUse { + discardExact(1) + val headLength = readInt() + discardExact(4) + val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength) + check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" } } } } @@ -223,6 +221,87 @@ internal object HighwayHelper { } } +internal class ChunkedFlowSession( + private val input: InputStream, + private val buffer: ByteArray, + private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T +) : Closeable { + override fun close() { + input.close() + } + + private var offset = 0L + + @Suppress("BlockingMethodInNonBlockingContext") + internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse { + runBIO { + val size = input.read(buffer) + block(mapper(buffer, size, offset)) + offset += size + } + } +} + + +internal fun createImageDataPacketSequence( + // RequestDataTrans + client: QQAndroidClient, + command: String, + appId: Int, + dataFlag: Int = 4096, + commandId: Int, + localId: Int = 2052, + ticket: ByteArray, + data: ExternalResource, + fileMd5: ByteArray, + sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE +): ChunkedFlowSession { + ByteArrayPool.checkBufferSize(sizePerPacket) + // require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" } + + return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket)) { buffer, size, offset -> + val head = CSDataHighwayHead.ReqDataHighwayHead( + msgBasehead = CSDataHighwayHead.DataHighwayHead( + version = 1, + uin = client.uin.toString(), + command = command, + seq = when (commandId) { + 2 -> client.nextHighwayDataTransSequenceIdForGroup() + 1 -> client.nextHighwayDataTransSequenceIdForFriend() + 27 -> client.nextHighwayDataTransSequenceIdForApplyUp() + else -> error("illegal commandId: $commandId") + }, + retryTimes = 0, + appid = appId, + dataflag = dataFlag, + commandId = commandId, + localeId = localId + ), + msgSeghead = CSDataHighwayHead.SegHead( + // cacheAddr = 812157193, + datalength = size, + dataoffset = offset, + filesize = data.size, + serviceticket = ticket, + md5 = buffer.md5(0, size), + fileMd5 = fileMd5, + flag = 0, + rtcode = 0 + ), + reqExtendinfo = EMPTY_BYTE_ARRAY, + msgLoginSigHead = null + ).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer()) + + buildPacket { + writeByte(40) + writeInt(head.size) + writeInt(size) + writeFully(head) + writeFully(buffer, 0, size) + writeByte(41) + } + } +} internal suspend inline fun List>.retryWithServers( timeoutMillis: Long, @@ -249,6 +328,7 @@ internal suspend inline fun List>.retryWithServers( onFail(exception) } +internal fun Int.sizeToString() = this.toLong().sizeToString() internal fun Long.sizeToString(): String { return if (this < 1024) { "$this B" diff --git a/mirai-core/src/commonMain/kotlin/network/highway/highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/highway.kt deleted file mode 100644 index da80421e3..000000000 --- a/mirai-core/src/commonMain/kotlin/network/highway/highway.kt +++ /dev/null @@ -1,92 +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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - -package net.mamoe.mirai.internal.network.highway - -import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.buildPacket -import kotlinx.io.core.writeFully -import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead -import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY -import net.mamoe.mirai.internal.utils.ByteArrayPool -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils -import net.mamoe.mirai.internal.utils.io.serialization.toByteArray -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 - - -internal fun createImageDataPacketSequence( - // RequestDataTrans - client: QQAndroidClient, - command: String, - appId: Int, - dataFlag: Int = 4096, - commandId: Int, - localId: Int = 2052, - ticket: ByteArray, - data: ReusableInput, - fileMd5: ByteArray, - sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE -): ChunkedFlowSession { - ByteArrayPool.checkBufferSize(sizePerPacket) - // require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" } - - val session: ChunkedFlowSession = data.chunkedFlow(sizePerPacket) - - var offset = 0L - return session.map { chunkedInput -> - buildPacket { - val head = CSDataHighwayHead.ReqDataHighwayHead( - msgBasehead = CSDataHighwayHead.DataHighwayHead( - version = 1, - uin = client.uin.toString(), - command = command, - seq = when (commandId) { - 2 -> client.nextHighwayDataTransSequenceIdForGroup() - 1 -> client.nextHighwayDataTransSequenceIdForFriend() - 27 -> client.nextHighwayDataTransSequenceIdForApplyUp() - else -> error("illegal commandId: $commandId") - }, - retryTimes = 0, - appid = appId, - dataflag = dataFlag, - commandId = commandId, - localeId = localId - ), - msgSeghead = CSDataHighwayHead.SegHead( - // cacheAddr = 812157193, - datalength = chunkedInput.bufferSize, - dataoffset = offset, - filesize = data.size, - serviceticket = ticket, - md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize), - fileMd5 = fileMd5, - flag = 0, - rtcode = 0 - ), - reqExtendinfo = EMPTY_BYTE_ARRAY, - msgLoginSigHead = null - ).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer()) - - offset += chunkedInput.bufferSize - - writeByte(40) - writeInt(head.size) - writeInt(chunkedInput.bufferSize) - writeFully(head) - writeFully(chunkedInput.buffer, 0, chunkedInput.bufferSize) - writeByte(41) - } - } -} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index a8d5db02e..a82d790ff 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -25,13 +25,15 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.Heartbeat import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin import net.mamoe.mirai.internal.network.readUShortLVByteArray -import net.mamoe.mirai.internal.utils.* import net.mamoe.mirai.internal.utils.crypto.TEA import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey import net.mamoe.mirai.internal.utils.io.readPacketExact import net.mamoe.mirai.internal.utils.io.readString import net.mamoe.mirai.internal.utils.io.useBytes import net.mamoe.mirai.internal.utils.io.withUse +import net.mamoe.mirai.internal.utils.toInt +import net.mamoe.mirai.internal.utils.toReadPacket +import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.utils.* internal sealed class PacketFactory { @@ -348,7 +350,7 @@ internal object KnownPacketFactories { 1 -> { input.discardExact(4) input.useBytes { data, length -> - MiraiPlatformUtils.unzip(data, 0, length).let { + data.unzip(0, length).let { val size = it.toInt() if (size == it.size || size == it.size + 4) { it.toReadPacket(offset = 4) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt index 3e896c706..131a94c5b 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt @@ -16,11 +16,11 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.toByteArray import kotlinx.io.core.writeFully import net.mamoe.mirai.internal.network.protocol.LoginType -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils.NetworkType import net.mamoe.mirai.internal.utils.io.* import net.mamoe.mirai.internal.utils.toByteArray import net.mamoe.mirai.utils.currentTimeMillis +import net.mamoe.mirai.utils.md5 import kotlin.random.Random /** @@ -101,8 +101,10 @@ internal fun BytePacketBuilder.t106( guid?.requireSize(16) writeShortLVPacket { - encryptAndWrite(MiraiPlatformUtils.md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt() - .toByteArray())) { + encryptAndWrite( + (passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt() + .toByteArray()).md5() + ) { writeShort(4)//TGTGTVer writeInt(Random.nextInt()) writeInt(ssoVersion)//ssoVer @@ -335,7 +337,7 @@ internal fun BytePacketBuilder.t109( ) { writeShort(0x109) writeShortLVPacket { - writeFully(MiraiPlatformUtils.md5(androidId)) + writeFully(androidId.md5()) } shouldEqualsTo 16 } @@ -571,7 +573,7 @@ internal fun BytePacketBuilder.t187( ) { writeShort(0x187) writeShortLVPacket { - writeFully(MiraiPlatformUtils.md5(macAddress)) // may be md5 + writeFully(macAddress.md5()) // may be md5 } } @@ -581,7 +583,7 @@ internal fun BytePacketBuilder.t188( ) { writeShort(0x188) writeShortLVPacket { - writeFully(MiraiPlatformUtils.md5(androidId)) + writeFully(androidId.md5()) } shouldEqualsTo 16 } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt index fc6c8efc2..78ea7247a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt @@ -26,17 +26,18 @@ import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.message.data.ForwardMessage import net.mamoe.mirai.message.data.asMessageChain +import net.mamoe.mirai.utils.gzip +import net.mamoe.mirai.utils.md5 internal class MessageValidationData( val data: ByteArray, - val md5: ByteArray = MiraiPlatformUtils.md5(data) + val md5: ByteArray = data.md5() ) { override fun toString(): String { return "MessageValidationData(data=, md5=${md5.contentToString()})" @@ -88,7 +89,7 @@ internal fun Collection.calculateValidationDataForGroup( val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) - return MessageValidationData(MiraiPlatformUtils.gzip(bytes)) + return MessageValidationData(bytes.gzip()) } internal class MultiMsg { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt index b3005a3f8..0df1199eb 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/ConfigPushSvc.kt @@ -10,7 +10,6 @@ package net.mamoe.mirai.internal.network.protocol.packet.login import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.use import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import net.mamoe.mirai.event.AbstractEvent @@ -23,12 +22,12 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket -import net.mamoe.mirai.internal.utils.ByteArrayPool import net.mamoe.mirai.internal.utils.hexToBytes import net.mamoe.mirai.internal.utils.io.ProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.* import net.mamoe.mirai.internal.utils.io.withUse import net.mamoe.mirai.internal.utils.toReadPacket +import net.mamoe.mirai.utils.ByteArrayPool import net.mamoe.mirai.utils.verbose import net.mamoe.mirai.internal.network.protocol.data.jce.PushReq as PushReqJceStruct diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt index db1e69135..8bc12fc7a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.internal.network.protocol.packet.login +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.cancel import kotlinx.coroutines.sync.withLock import kotlinx.io.core.ByteReadPacket @@ -30,10 +31,13 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.* import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769 import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline import net.mamoe.mirai.internal.network.protocol.packet.* -import net.mamoe.mirai.internal.utils.* +import net.mamoe.mirai.internal.utils.NetworkType +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.utils.encodeToString import net.mamoe.mirai.internal.utils.io.serialization.* import net.mamoe.mirai.utils.currentTimeMillis -import java.util.concurrent.CancellationException +import net.mamoe.mirai.internal.utils.toReadPacket +import net.mamoe.mirai.utils.localIpAddress @Suppress("EnumEntryName", "unused") internal enum class RegPushReason { @@ -157,7 +161,7 @@ internal class StatSvc { strDevType = client.device.model.encodeToString(), strOSVer = client.device.version.release.encodeToString(), uOldSSOIp = 0, - uNewSSOIp = MiraiPlatformUtils.localIpAddress().runCatching { ipToLong() } + uNewSSOIp = localIpAddress().runCatching { ipToLong() } .getOrElse { "192.168.1.123".ipToLong() }, strVendorName = "MIUI", strVendorOSName = "?ONEPLUS A5000_23_17", diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt index ab7ed0fc0..5828f35bf 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt @@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.utils.io.* import net.mamoe.mirai.utils.currentTimeSeconds import net.mamoe.mirai.utils.error import net.mamoe.mirai.utils.generateDeviceInfoData +import net.mamoe.mirai.utils.md5 internal class WtLogin { /** @@ -80,7 +81,7 @@ internal class WtLogin { t8(2052) t104(client.t104) t116(client.miscBitMap, client.subSigMap) - t401(MiraiPlatformUtils.md5(client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402)) + t401((client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402).md5()) } } } diff --git a/mirai-core/src/commonMain/kotlin/utils/ByteArrayPool.kt b/mirai-core/src/commonMain/kotlin/utils/ByteArrayPool.kt deleted file mode 100644 index a366b4f21..000000000 --- a/mirai-core/src/commonMain/kotlin/utils/ByteArrayPool.kt +++ /dev/null @@ -1,18 +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 - */ - -package net.mamoe.mirai.internal.utils - -import kotlinx.io.pool.ObjectPool - -/** - * 缓存 [ByteArray] 实例的 [ObjectPool] - */ -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -internal typealias ByteArrayPool = net.mamoe.mirai.utils.internal.ByteArrayPool \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/MiraiPlatformUtils.kt b/mirai-core/src/commonMain/kotlin/utils/MiraiPlatformUtils.kt deleted file mode 100644 index fbcc813e1..000000000 --- a/mirai-core/src/commonMain/kotlin/utils/MiraiPlatformUtils.kt +++ /dev/null @@ -1,114 +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 - */ - -package net.mamoe.mirai.internal.utils - -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.util.* -import kotlinx.io.pool.useInstance -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.io.OutputStream -import java.net.Inet4Address -import java.security.MessageDigest -import java.util.zip.Deflater -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream -import java.util.zip.Inflater - -internal object MiraiPlatformUtils { - fun unzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray { - data.checkOffsetAndLength(offset, length) - if (length == 0) return ByteArray(0) - - val inflater = Inflater() - inflater.reset() - ByteArrayOutputStream().use { output -> - inflater.setInput(data, offset, length) - ByteArrayPool.useInstance { - while (!inflater.finished()) { - output.write(it, 0, inflater.inflate(it)) - } - } - - inflater.end() - return output.toByteArray() - } - } - - fun zip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray { - data.checkOffsetAndLength(offset, length) - if (length == 0) return ByteArray(0) - - val deflater = Deflater() - deflater.setInput(data, offset, length) - deflater.finish() - - ByteArrayPool.useInstance { - return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() } - } - } - - fun gzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray { - ByteArrayOutputStream().use { buf -> - GZIPOutputStream(buf).use { gzip -> - data.inputStream(offset, length).use { t -> t.copyTo(gzip) } - } - buf.flush() - return buf.toByteArray() - } - } - - fun ungzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray { - return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() } - } - - fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray { - data.checkOffsetAndLength(offset, length) - return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest() - } - - fun md5(str: String): ByteArray = md5(str.toByteArray()) - - /** - * Ktor HttpClient. 不同平台使用不同引擎. - */ - @OptIn(KtorExperimentalAPI::class) - val Http: HttpClient = HttpClient(CIO) - - /** - * Localhost 解析 - */ - fun localIpAddress(): String = kotlin.runCatching { - Inet4Address.getLocalHost().hostAddress - }.getOrElse { "192.168.1.123" } - - fun md5(stream: InputStream): ByteArray { - val digest = MessageDigest.getInstance("md5") - digest.reset() - stream.use { input -> - object : OutputStream() { - override fun write(b: Int) { - digest.update(b.toByte()) - } - }.use { output -> - input.copyTo(output) - } - } - return digest.digest() - } -} - -@Suppress("DuplicatedCode") // false positive. `this` is not the same for `List` and `ByteArray` -internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) { - require(offset >= 0) { "offset shouldn't be negative: $offset" } - require(length >= 0) { "length shouldn't be negative: $length" } - require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } -} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/byteArrays.kt b/mirai-core/src/commonMain/kotlin/utils/byteArrays.kt index 31e8d7205..5c28aac3b 100644 --- a/mirai-core/src/commonMain/kotlin/utils/byteArrays.kt +++ b/mirai-core/src/commonMain/kotlin/utils/byteArrays.kt @@ -17,14 +17,11 @@ import kotlinx.io.charsets.Charset import kotlinx.io.charsets.Charsets import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.String -import kotlinx.io.core.use +import net.mamoe.mirai.internal.utils.io.withUse +import net.mamoe.mirai.utils.checkOffsetAndLength import java.util.* import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.jvm.JvmMultifileClass -import kotlin.jvm.JvmName -import kotlin.jvm.JvmOverloads -import kotlin.jvm.JvmSynthetic @JvmOverloads @@ -107,5 +104,5 @@ internal inline fun ByteArray.read(t: ByteReadPacket.() -> R): R { contract { callsInPlace(t, InvocationKind.EXACTLY_ONCE) } - return this.toReadPacket().use(t) + return this.toReadPacket().withUse(t) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt b/mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt index adef059ca..5c525d205 100644 --- a/mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt +++ b/mirai-core/src/commonMain/kotlin/utils/crypto/TEA.kt @@ -10,10 +10,9 @@ package net.mamoe.mirai.internal.utils.crypto import kotlinx.io.core.ByteReadPacket -import kotlinx.io.pool.useInstance -import net.mamoe.mirai.internal.utils.ByteArrayPool import net.mamoe.mirai.internal.utils.toByteArray import net.mamoe.mirai.internal.utils.toUHexString +import net.mamoe.mirai.utils.ByteArrayPool import kotlin.experimental.and import kotlin.experimental.xor import kotlin.random.Random diff --git a/mirai-core/src/commonMain/kotlin/utils/io/input.kt b/mirai-core/src/commonMain/kotlin/utils/io/input.kt index bffcdaa9a..55132cf70 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/input.kt +++ b/mirai-core/src/commonMain/kotlin/utils/io/input.kt @@ -16,14 +16,11 @@ package net.mamoe.mirai.internal.utils.io import kotlinx.io.charsets.Charset import kotlinx.io.charsets.Charsets import kotlinx.io.core.* -import net.mamoe.mirai.internal.utils.ByteArrayPool import net.mamoe.mirai.internal.utils.toReadPacket import net.mamoe.mirai.internal.utils.toUHexString +import net.mamoe.mirai.utils.ByteArrayPool import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.jvm.JvmMultifileClass -import kotlin.jvm.JvmName -import kotlin.jvm.JvmSynthetic @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") internal inline fun ByteReadPacket.useBytes( diff --git a/mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt b/mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt index 1f7d659d0..c58d81c1f 100644 --- a/mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt +++ b/mirai-core/src/commonTest/kotlin/PlatformUtilsTest.kt @@ -9,6 +9,10 @@ package net.mamoe.mirai.internal.utils import kotlinx.io.core.toByteArray +import net.mamoe.mirai.utils.gzip +import net.mamoe.mirai.utils.ungzip +import net.mamoe.mirai.utils.unzip +import net.mamoe.mirai.utils.zip import kotlin.test.Test import kotlin.test.assertEquals @@ -16,11 +20,11 @@ internal class PlatformUtilsTest { @Test fun testZip() { - assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString()) + assertEquals("test", "test".toByteArray().zip().unzip().encodeToString()) } @Test fun testGZip() { - assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString()) + assertEquals("test", "test".toByteArray().gzip().ungzip().encodeToString()) } } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/test/printing.kt b/mirai-core/src/commonTest/kotlin/test/printing.kt index 103fd7ad4..5b30d65a9 100644 --- a/mirai-core/src/commonTest/kotlin/test/printing.kt +++ b/mirai-core/src/commonTest/kotlin/test/printing.kt @@ -14,10 +14,10 @@ package test import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.Input import kotlinx.io.core.readAvailable -import kotlinx.io.pool.useInstance -import net.mamoe.mirai.internal.utils.ByteArrayPool +import kotlinx.io.core.use import net.mamoe.mirai.internal.utils.toReadPacket import net.mamoe.mirai.internal.utils.toUHexString +import net.mamoe.mirai.utils.ByteArrayPool import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLoggerWithSwitch import net.mamoe.mirai.utils.withSwitch diff --git a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt b/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt index 0203be8f7..f197ba212 100644 --- a/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt +++ b/mirai-core/src/jvmMain/kotlin/utils/crypto/ECDHJvmDesktop.kt @@ -9,7 +9,7 @@ package net.mamoe.mirai.internal.utils.crypto -import net.mamoe.mirai.internal.utils.MiraiPlatformUtils +import net.mamoe.mirai.utils.md5 import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.* import java.security.spec.ECGenParameterSpec @@ -81,7 +81,7 @@ internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { val instance = KeyAgreement.getInstance("ECDH", "BC") instance.init(privateKey) instance.doPhase(publicKey, true) - return MiraiPlatformUtils.md5(instance.generateSecret()) + return instance.generateSecret().md5() } actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {