ExternalResource (#754)

* ExternalResource fundamentals:
- Introduce ExternalResource
- Migrate functions
- Move utilities to mirai-core-utils

* Fix build

* Fix filename and misc improvements

* Close file on ExternalResource.close;
Reset filePointer to 0 on stream close

* Rearrange image extensions

* Fix tests

* Fix build

* toExternalResource: formatName = null by default

* Reduce unnecessary continuations

* Fix ExternalResourceImplByFileWithMd5.inputStream

* ExternalResource: Remove BufferedImage support

* Don't close stream on image upload;
Unified closing behaviorImprove;
Improve FileCacheStrategy;

* Fix createImageDataPacketSequence closing

* Fix image upload, change size to long

* Fix docs

* Rename SendImageUtilsJvmKt to SendResourceUtilsJvmKt

* Run BIO appropriately

* Postpone file detection on formatName getter

* Fix SendResourceUtilsJvmKt JvmName

Co-authored-by: Karlatemp <karlatemp@vip.qq.com>
This commit is contained in:
Him188 2020-12-26 17:36:00 +08:00 committed by GitHub
parent c3bbabc274
commit bfda72e58f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1002 additions and 1446 deletions

View File

@ -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 内部协议区分, 一般人使用时无需在意.
*/

View File

@ -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 : Contact> C.sendImage(imageStream: InputStream): MessageReceipt<C> =
imageStream.sendAsImageTo(this)
/**
* 将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
* @see FileCacheStrategy
*/
@Throws(OverFileSizeMaxException::class)
@JvmStatic
@JvmBlockingBridge
public suspend fun <C : Contact> C.sendImage(file: File): MessageReceipt<C> = file.sendAsImageTo(this)
}
}
/**

View File

@ -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<Group> =
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
/**
* 上传一个语音消息以备发送.
* 请手动关闭输入流

View File

@ -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.")
}
}

View File

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

View File

@ -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<Contact> =
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<Contact> = this.sendTo(subject)
public override suspend fun ExternalResource.sendAsImage(): MessageReceipt<Contact> = this.sendAsImageTo(subject)
public override suspend fun Image.send(): MessageReceipt<Contact> = 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<Contact> = subject.sendImage(image)
public override suspend fun sendImage(image: InputStream): MessageReceipt<Contact> = subject.sendImage(image)
public override suspend fun sendImage(image: File): MessageReceipt<Contact> = 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<Contact> = sendTo(subject)
public override suspend fun InputStream.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
public override suspend fun File.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
// endregion 发送图片 (扩展)
@ -773,10 +771,10 @@ public interface MessageEventExtensions<out TSender : User, out TSubject : Conta
// endregion
@JvmSynthetic
public suspend fun ExternalImage.upload(): Image
public suspend fun ExternalResource.uploadAsImage(): Image
@JvmSynthetic
public suspend fun ExternalImage.send(): MessageReceipt<TSubject>
public suspend fun ExternalResource.sendAsImage(): MessageReceipt<TSubject>
@JvmSynthetic
public suspend fun Image.send(): MessageReceipt<TSubject>
@ -828,9 +826,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// region 上传图片
@JvmBlockingBridge
public suspend fun uploadImage(image: BufferedImage): Image
@JvmBlockingBridge
public suspend fun uploadImage(image: InputStream): Image
@ -839,9 +834,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion
// region 发送图片
@JvmBlockingBridge
public suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject>
@JvmBlockingBridge
public suspend fun sendImage(image: InputStream): MessageReceipt<TSubject>
@ -850,9 +842,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion
// region 上传图片 (扩展)
@JvmSynthetic
public suspend fun BufferedImage.upload(): Image
@JvmSynthetic
public suspend fun InputStream.uploadAsImage(): Image
@ -861,9 +850,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion 上传图片 (扩展)
// region 发送图片 (扩展)
@JvmSynthetic
public suspend fun BufferedImage.send(): MessageReceipt<TSubject>
@JvmSynthetic
public suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject>

View File

@ -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 : CustomMessageMetadata> T.customToStringImpl(factory: Cu
@Suppress("UNCHECKED_CAST")
return (factory as CustomMessage.Factory<T>).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)
}
}
}
}

View File

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

View File

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

View File

@ -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 <C : Contact> ExternalImage.sendTo(contact: C): MessageReceipt<C> = 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 : Contact> C.sendImage(image: ExternalImage): MessageReceipt<C> = 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()
}
}

View File

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

View File

@ -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 <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
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 : Contact> C.sendImage(image: ExternalResource): MessageReceipt<C> =
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
*/

View File

@ -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))
outputStream().use { out -> input.copyTo(out) }
}.toExternalResource(formatName)
}
}
public companion object {
/**
* 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai [IMirai.FileCacheStrategy] 获取.
*
* @see IMirai.FileCacheStrategy
*/
@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))
}
@JvmStatic
public val PlatformDefault: FileCacheStrategy = TempCache(null)
}
}
@Throws(java.io.IOException::class)
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = readAvailable(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = readAvailable(buffer)
}
return bytesCopied
}
internal inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return use { output.use { block(this, output) } }
}

View File

@ -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 <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
runBIO {
@Suppress("BlockingMethodInNonBlockingContext")
toExternalResource("png")
}.withUse { sendAsImageTo(contact) }
/**
* 将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmSynthetic
public suspend inline fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
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

View File

@ -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<T> : Closeable {
public val flow: Flow<T>
override fun close()
}
internal inline fun <T, R> ChunkedFlowSession<T>.map(crossinline mapper: suspend ChunkedFlowSession<T>.(T) -> R): ChunkedFlowSession<R> {
return object : ChunkedFlowSession<R> {
override val flow: Flow<R> = 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<ChunkedInput> {
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<ChunkedInput> {
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<ChunkedInput> {
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<ChunkedInput> {
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)
}
}
}

View File

@ -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<ChunkedInput> {
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
}

View File

@ -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<ChunkedInput>
suspend fun writeTo(out: ByteWriteChannel): Long
/**
* Remember to close.
*/
fun asInput(): Input
fun release()
}

View File

@ -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<ChunkedInput> {
return object : ChunkedFlowSession<ChunkedInput> {
private val stream = inputStream()
override val flow: Flow<ChunkedInput> = 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<ChunkedInput> {
val stream = inputStream()
return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = 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<ChunkedInput> {
val stream = inputStream()
return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = 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
}

View File

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

View File

@ -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 <C : Contact> BufferedImage.sendTo(contact: C): MessageReceipt<C> =
toExternalImage().sendTo(contact)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
public suspend fun <C : Contact> InputStream.sendAsImageTo(contact: C): MessageReceipt<C> =
toExternalImage().sendTo(contact)
/**
* [Dispatchers.IO] 中将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
public suspend fun <C : Contact> File.sendAsImageTo(contact: C): MessageReceipt<C> {
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 : Contact> C.sendImage(bufferedImage: BufferedImage): MessageReceipt<C> =
bufferedImage.sendTo(this)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
public suspend inline fun <C : Contact> C.sendImage(imageStream: InputStream): MessageReceipt<C> =
imageStream.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
public suspend inline fun <C : Contact> C.sendImage(file: File): MessageReceipt<C> = 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

View File

@ -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<Byte>` 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<ByteArray>(256) {
public object ByteArrayPool : DefaultPool<ByteArray>(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 <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
public inline fun <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
if (requestedSize > BUFFER_SIZE) {
return ByteArray(requestedSize).run(block)
}

View File

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

View File

@ -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)
}
public suspend inline fun <R> runBIO(
noinline block: suspend CoroutineScope.() -> R
): R = withContext(Dispatchers.IO, block)

View File

@ -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<String, String> = 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
}

View File

@ -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 : Closeable, R> 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 : AutoCloseable, O : AutoCloseable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return use { output.use { block(this, output) } }
}

View File

@ -6,6 +6,7 @@
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.utils
import kotlin.test.Test

View File

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

View File

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

View File

@ -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<LongConn.OffPicUp.Response>()
}
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()
}
}

View File

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

View File

@ -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<QQAndroidBotNetworkHandler, Image> {
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

View File

@ -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.")
}

View File

@ -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<ImMsgBody.Elem>.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<ImMsgBody.Elem>.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]}")
}
}

View File

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

View File

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

View File

@ -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<HttpStatusCode> {
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<Pair<Int, Int>>,
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<Pair<Int, Int>>,
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,8 +157,7 @@ internal object HighwayHelper {
ticket = ticket,
data = imageInput,
fileMd5 = fileMd5
).withUse {
flow.collect {
).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
@ -172,7 +171,6 @@ internal object HighwayHelper {
}
}
}
}
suspend fun uploadPttToServers(
bot: QQAndroidBot,
@ -223,6 +221,87 @@ internal object HighwayHelper {
}
}
internal class ChunkedFlowSession<T>(
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<ByteReadPacket> {
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<Pair<Int, Int>>.retryWithServers(
timeoutMillis: Long,
@ -249,6 +328,7 @@ internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
onFail(exception)
}
internal fun Int.sizeToString() = this.toLong().sizeToString()
internal fun Long.sizeToString(): String {
return if (this < 1024) {
"$this B"

View File

@ -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<ByteReadPacket> {
ByteArrayPool.checkBufferSize(sizePerPacket)
// require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
val session: ChunkedFlowSession<ChunkedInput> = 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)
}
}
}

View File

@ -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<TPacket : Packet?> {
@ -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)

View File

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

View File

@ -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=<size=${data.size}>, md5=${md5.contentToString()})"
@ -88,7 +89,7 @@ internal fun Collection<ForwardMessage.INode>.calculateValidationDataForGroup(
val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
return MessageValidationData(MiraiPlatformUtils.gzip(bytes))
return MessageValidationData(bytes.gzip())
}
internal class MultiMsg {

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Byte>` 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})" }
}

View File

@ -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 <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
contract {
callsInPlace(t, InvocationKind.EXACTLY_ONCE)
}
return this.toReadPacket().use(t)
return this.toReadPacket().withUse(t)
}

View File

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

View File

@ -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 <R> ByteReadPacket.useBytes(

View File

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

View File

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

View File

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