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.*
import net.mamoe.mirai.message.data.Image.Key.queryUrl import net.mamoe.mirai.message.data.Image.Key.queryUrl
import net.mamoe.mirai.message.data.MessageSource.Key.recall 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.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
@ -35,7 +36,7 @@ public val Mirai: IMirai by lazy { findMiraiInstance() }
/** /**
* Mirai API 接口. * Mirai API 接口.
* *
* @see Mirai * @see Mirai 获取实例
*/ */
public interface IMirai : LowLevelApiAccessor { public interface IMirai : LowLevelApiAccessor {
/** /**
@ -47,6 +48,13 @@ public interface IMirai : LowLevelApiAccessor {
@MiraiExperimentalApi @MiraiExperimentalApi
public val BotFactory: BotFactory public val BotFactory: BotFactory
/**
* Mirai 全局使用的 [FileCacheStrategy].
*/
@Suppress("PropertyName")
@MiraiExperimentalApi
public var FileCacheStrategy: FileCacheStrategy
/** /**
* 使用 groupCode 计算 groupUin. 这两个值仅在 mirai 内部协议区分, 一般人使用时无需在意. * 使用 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.quote
import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.MessageReceipt.Companion.recall
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.OverFileSizeMaxException import java.io.File
import net.mamoe.mirai.utils.WeakRefProperty import java.io.InputStream
/** /**
* 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], [][Group]. * 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], [][Group].
@ -70,21 +70,51 @@ public interface Contact : ContactOrBot, CoroutineScope {
/** /**
* 上传一个图片以备发送. * 上传一个图片以备发送.
* *
* 无论上传是否成功都不会关闭 [resource].
*
* @see Image 查看有关图片的更多信息, 如上传图片 * @see Image 查看有关图片的更多信息, 如上传图片
* *
* @see BeforeImageUploadEvent 图片发送前事件, 可拦截. * @see BeforeImageUploadEvent 图片发送前事件, 可拦截.
* @see ImageUploadEvent 图片发送完成事件, 不可拦截. * @see ImageUploadEvent 图片发送完成事件, 不可拦截.
* *
* @see ExternalResource
*
* @throws EventCancelledException 当发送消息事件被取消时抛出 * @throws EventCancelledException 当发送消息事件被取消时抛出
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, mirai 限制的大小为 30 MB) * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, mirai 限制的大小为 30 MB)
*/ */
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun uploadImage(image: ExternalImage): Image public suspend fun uploadImage(resource: ExternalResource): Image
/** /**
* @return "Friend($id)" or "Group($id)" or "Member($id)" * @return "Friend($id)" or "Group($id)" or "Member($id)"
*/ */
public override fun toString(): String 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
import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.MessageReceipt.Companion.recall
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.OverFileSizeMaxException import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.PlannedRemoval
@ -162,21 +161,6 @@ public interface Group : Contact, CoroutineScope {
public override suspend fun sendMessage(message: String): MessageReceipt<Group> = public override suspend fun sendMessage(message: String): MessageReceipt<Group> =
this.sendMessage(message.toPlainText()) 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.message.data.Message
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE 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.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
@ -42,7 +42,7 @@ public interface OtherClient : Contact {
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.") 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.") throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.")
} }
} }

View File

@ -14,18 +14,17 @@ package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.Bot 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
import net.mamoe.mirai.message.MessageReceipt.Companion.recall import net.mamoe.mirai.message.MessageReceipt.Companion.recall
import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.action.UserNudge 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.Message
import net.mamoe.mirai.message.data.isContentEmpty import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.message.data.toPlainText import net.mamoe.mirai.message.data.toPlainText
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.OverFileSizeMaxException
/** /**
* 代表一个 **用户**. * 代表一个 **用户**.
@ -92,20 +91,6 @@ public interface User : Contact, UserOrBot, CoroutineScope {
*/ */
@MiraiExperimentalApi @MiraiExperimentalApi
public override fun nudge(): UserNudge 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.kjbb.JvmBlockingBridge
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.Contact.Companion.sendImage
import net.mamoe.mirai.event.* import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.events.ImageUploadEvent.Failed import net.mamoe.mirai.event.events.ImageUploadEvent.Failed
import net.mamoe.mirai.event.events.ImageUploadEvent.Succeed 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.data.MessageSource.Key.quote
import net.mamoe.mirai.message.isContextIdenticalWith import net.mamoe.mirai.message.isContextIdenticalWith
import net.mamoe.mirai.utils.* 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.File
import java.io.InputStream import java.io.InputStream
import kotlin.internal.InlineOnly import kotlin.internal.InlineOnly
@ -415,7 +417,7 @@ public val MessageRecallEvent.isByBot: Boolean
*/ */
public data class BeforeImageUploadEvent @MiraiInternalApi constructor( public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
public val target: Contact, public val target: Contact,
public val source: ExternalImage public val source: ExternalResource
) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent { ) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent {
public override val bot: Bot public override val bot: Bot
get() = target.bot get() = target.bot
@ -434,19 +436,19 @@ public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
*/ */
public sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractEvent() { public sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractEvent() {
public abstract val target: Contact public abstract val target: Contact
public abstract val source: ExternalImage public abstract val source: ExternalResource
public override val bot: Bot public override val bot: Bot
get() = target.bot get() = target.bot
public data class Succeed @MiraiInternalApi constructor( public data class Succeed @MiraiInternalApi constructor(
override val target: Contact, override val target: Contact,
override val source: ExternalImage, override val source: ExternalResource,
val image: Image val image: Image
) : ImageUploadEvent() ) : ImageUploadEvent()
public data class Failed @MiraiInternalApi constructor( public data class Failed @MiraiInternalApi constructor(
override val target: Contact, override val target: Contact,
override val source: ExternalImage, override val source: ExternalResource,
val errno: Int, val errno: Int,
val message: String val message: String
) : ImageUploadEvent() ) : ImageUploadEvent()
@ -631,9 +633,9 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
public override suspend fun reply(plain: String): MessageReceipt<Contact> = public override suspend fun reply(plain: String): MessageReceipt<Contact> =
subject.sendMessage(PlainText(plain).asMessageChain()) 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) public override suspend fun Image.send(): MessageReceipt<Contact> = this.sendTo(subject)
@ -667,25 +669,21 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
// region 上传图片 // 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: InputStream): Image = subject.uploadImage(image)
public override suspend fun uploadImage(image: File): Image = subject.uploadImage(image) public override suspend fun uploadImage(image: File): Image = subject.uploadImage(image)
// endregion // endregion
// region 发送图片 // 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: InputStream): MessageReceipt<Contact> = subject.sendImage(image)
public override suspend fun sendImage(image: File): MessageReceipt<Contact> = subject.sendImage(image) public override suspend fun sendImage(image: File): MessageReceipt<Contact> = subject.sendImage(image)
// endregion // endregion
// region 上传图片 (扩展) // region 上传图片 (扩展)
public override suspend fun BufferedImage.upload(): Image = upload(subject)
public override suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject) public override suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
public override suspend fun File.uploadAsImage(): Image = uploadAsImage(subject) public override suspend fun File.uploadAsImage(): Image = uploadAsImage(subject)
// endregion 上传图片 (扩展) // endregion 上传图片 (扩展)
// region 发送图片 (扩展) // 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 InputStream.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
public override suspend fun File.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject) public override suspend fun File.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
// endregion 发送图片 (扩展) // endregion 发送图片 (扩展)
@ -773,10 +771,10 @@ public interface MessageEventExtensions<out TSender : User, out TSubject : Conta
// endregion // endregion
@JvmSynthetic @JvmSynthetic
public suspend fun ExternalImage.upload(): Image public suspend fun ExternalResource.uploadAsImage(): Image
@JvmSynthetic @JvmSynthetic
public suspend fun ExternalImage.send(): MessageReceipt<TSubject> public suspend fun ExternalResource.sendAsImage(): MessageReceipt<TSubject>
@JvmSynthetic @JvmSynthetic
public suspend fun Image.send(): MessageReceipt<TSubject> public suspend fun Image.send(): MessageReceipt<TSubject>
@ -828,9 +826,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// region 上传图片 // region 上传图片
@JvmBlockingBridge
public suspend fun uploadImage(image: BufferedImage): Image
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun uploadImage(image: InputStream): Image public suspend fun uploadImage(image: InputStream): Image
@ -839,9 +834,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion // endregion
// region 发送图片 // region 发送图片
@JvmBlockingBridge
public suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject>
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun sendImage(image: InputStream): MessageReceipt<TSubject> public suspend fun sendImage(image: InputStream): MessageReceipt<TSubject>
@ -850,9 +842,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion // endregion
// region 上传图片 (扩展) // region 上传图片 (扩展)
@JvmSynthetic
public suspend fun BufferedImage.upload(): Image
@JvmSynthetic @JvmSynthetic
public suspend fun InputStream.uploadAsImage(): Image public suspend fun InputStream.uploadAsImage(): Image
@ -861,9 +850,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
// endregion 上传图片 (扩展) // endregion 上传图片 (扩展)
// region 发送图片 (扩展) // region 发送图片 (扩展)
@JvmSynthetic
public suspend fun BufferedImage.send(): MessageReceipt<TSubject>
@JvmSynthetic @JvmSynthetic
public suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject> public suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject>

View File

@ -19,7 +19,6 @@ import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.message.MessageSerializer import net.mamoe.mirai.message.MessageSerializer
import net.mamoe.mirai.utils.* 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") @Suppress("UNCHECKED_CAST")
return (factory as CustomMessage.Factory<T>).dump(this) 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 public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub
/** 缓存策略 */
@MiraiExperimentalApi
public var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault
/** /**
* Json 序列化器, 使用 'kotlinx.serialization' * Json 序列化器, 使用 'kotlinx.serialization'
*/ */
@ -270,7 +266,6 @@ public open class BotConfiguration { // open for Java
new.reconnectionRetryTimes = reconnectionRetryTimes new.reconnectionRetryTimes = reconnectionRetryTimes
new.loginSolver = loginSolver new.loginSolver = loginSolver
new.protocol = protocol 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.getRandomByteArray
import net.mamoe.mirai.utils.internal.getRandomIntString import net.mamoe.mirai.utils.internal.getRandomIntString
import net.mamoe.mirai.utils.internal.getRandomString import net.mamoe.mirai.utils.internal.getRandomString
import net.mamoe.mirai.utils.internal.md5
import java.io.File import java.io.File
/** /**
@ -78,7 +77,7 @@ public class DeviceInfo(
model = "mirai".toByteArray(), model = "mirai".toByteArray(),
bootloader = "unknown".toByteArray(), bootloader = "unknown".toByteArray(),
fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${getRandomIntString(7)}:user/release-keys".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(), procVersion = "Linux version 3.0.31-${getRandomString(8)} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
baseBand = byteArrayOf(), baseBand = byteArrayOf(),
version = Version(), 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 package net.mamoe.mirai.utils
import kotlinx.io.core.* import kotlinx.coroutines.Dispatchers
import net.mamoe.mirai.Bot import net.mamoe.mirai.IMirai
import net.mamoe.mirai.utils.internal.asReusableInput import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream 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 { public interface FileCacheStrategy {
/** /**
* [input] 缓存为 [ExternalImage]. * 立即读取 [input] 所有内容并缓存为 [ExternalResource].
* 此函数应 close 这个 [Input] *
* 注意:
* - 此函数不会关闭输入
* - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
*
* @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai"
*/ */
@MiraiExperimentalApi @Throws(IOException::class)
@Throws(java.io.IOException::class) public fun newCache(input: InputStream, formatName: String? = null): ExternalResource
public fun newImageCache(input: Input): ExternalImage
/** /**
* [input] 缓存为 [ExternalImage]. * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai".
* 此函数应 close 这个 [InputStream] *
* 注意:
* - 此函数不会关闭输入
* - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
*/ */
@MiraiExperimentalApi @Throws(IOException::class)
@Throws(java.io.IOException::class) public fun newCache(input: InputStream): ExternalResource = newCache(input, null)
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)
/** /**
* 使用内存直接存储所有图片文件. * 使用内存直接存储所有图片文件.
*/ */
public object MemoryCache : FileCacheStrategy { public object MemoryCache : FileCacheStrategy {
@MiraiExperimentalApi @Throws(IOException::class)
@Throws(java.io.IOException::class) override fun newCache(input: InputStream, formatName: String?): ExternalResource {
override fun newImageCache(input: Input): ExternalImage { return input.readBytes().toExternalResource(formatName)
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())
} }
} }
@ -124,87 +73,27 @@ public interface FileCacheStrategy {
*/ */
public val directory: File? = null public val directory: File? = null
) : FileCacheStrategy { ) : FileCacheStrategy {
@MiraiExperimentalApi private fun createTempFile(): File {
@Throws(java.io.IOException::class) return File.createTempFile("tmp", null, directory)
override fun newImageCache(input: Input): ExternalImage { }
return ExternalImage(File.createTempFile("tmp", null, directory).apply {
@Throws(IOException::class)
override fun newCache(input: InputStream, formatName: String?): ExternalResource {
return createTempFile().apply {
deleteOnExit() deleteOnExit()
input.withOut(this.outputStream()) { copyTo(it) } outputStream().use { out -> input.copyTo(out) }
}.asReusableInput(true)) }.toExternalResource(formatName)
}
@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))
} }
} }
}
public companion object {
@Throws(java.io.IOException::class) /**
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long { * 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai [IMirai.FileCacheStrategy] 获取.
var bytesCopied: Long = 0 *
val buffer = ByteArray(bufferSize) * @see IMirai.FileCacheStrategy
var bytes = readAvailable(buffer) */
while (bytes >= 0) { @MiraiExperimentalApi
out.write(buffer, 0, bytes) @JvmStatic
bytesCopied += bytes public val PlatformDefault: FileCacheStrategy = TempCache(null)
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 * 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.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] * 缓存 [ByteArray] 实例的 [ObjectPool]
*/ */
internal object ByteArrayPool : DefaultPool<ByteArray>(256) { public object ByteArrayPool : DefaultPool<ByteArray>(256) {
/** /**
* 每一个 [ByteArray] 的大小 * 每一个 [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 produceInstance(): ByteArray = ByteArray(BUFFER_SIZE)
override fun clearInstance(instance: ByteArray): ByteArray = instance 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" } 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" } require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" }
} }
/** /**
* 请求一个大小至少为 [requestedSize] [ByteArray] 实例. * 请求一个大小至少为 [requestedSize] [ByteArray] 实例.
*/ // 不要写为扩展函数. 它需要优先于 kotlinx.io 的扩展函数 resolve */ // 不要写为扩展函数. 它需要优先于 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) { if (requestedSize > BUFFER_SIZE) {
return ByteArray(requestedSize).run(block) 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 * 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 { import kotlinx.coroutines.CoroutineScope
val initialized: Boolean import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
public suspend inline fun <R> runBIO(
suspend fun init(strategy: FileCacheStrategy) 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 * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.test.Test import kotlin.test.Test

View File

@ -10,10 +10,8 @@
package net.mamoe.mirai.internal package net.mamoe.mirai.internal
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import kotlin.jvm.JvmSynthetic import net.mamoe.mirai.utils.md5
internal data class BotAccount( internal data class BotAccount(
@JvmSynthetic @JvmSynthetic
@ -22,7 +20,7 @@ internal data class BotAccount(
@MiraiExperimentalApi @MiraiExperimentalApi
val passwordMd5: ByteArray // md5 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || this::class != other::class) return false 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.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList 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.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.encodeToString
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt 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_1
import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2 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.Image.Key.GROUP_IMAGE_ID_REGEX
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.currentTimeSeconds
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.random.Random import kotlin.random.Random
@ -62,6 +59,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
override val BotFactory: BotFactory override val BotFactory: BotFactory
get() = BotFactoryImpl get() = BotFactoryImpl
override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
@OptIn(LowLevelApi::class) @OptIn(LowLevelApi::class)
override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) { override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@ -609,8 +608,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
bot, bot,
response.proto.uint32UpIp.zip(response.proto.uint32UpPort), response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
response.proto.msgSig, response.proto.msgSig,
MiraiPlatformUtils.md5(body), body.md5(),
net.mamoe.mirai.utils.internal.asReusableInput0(body), // don't use toLongUnsigned: Overload resolution ambiguity body.toExternalResource(null), // don't use toLongUnsigned: Overload resolution ambiguity
"group long message", "group long message",
27 27
) )
@ -801,7 +800,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
else -> error("Internal error: unsupported image class: ${image::class.simpleName}") 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 { override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) { if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
throw UnsupportedOperationException("nudge is supported only with protocol 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.BeforeImageUploadEvent
import net.mamoe.mirai.event.events.EventCancelledException import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent 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.postImage
import net.mamoe.mirai.internal.network.highway.sizeToString 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.data.proto.Cmd0x352
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn 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.internal.utils.toUHexString
import net.mamoe.mirai.message.data.Image 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 net.mamoe.mirai.utils.verbose
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -38,11 +39,8 @@ internal abstract class AbstractUser(
final override val remark: String = friendInfo.remark final override val remark: String = friendInfo.remark
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
override suspend fun uploadImage(image: ExternalImage): Image = try { override suspend fun uploadImage(resource: ExternalResource): Image {
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
image.input.init(bot.configuration.fileCacheStrategy)
}
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
} }
val response = bot.network.run { val response = bot.network.run {
@ -51,22 +49,22 @@ internal abstract class AbstractUser(
srcUin = bot.id.toInt(), srcUin = bot.id.toInt(),
dstUin = id.toInt(), dstUin = id.toInt(),
fileId = 0, fileId = 0,
fileMd5 = image.md5, fileMd5 = resource.md5,
fileSize = image.input.size.toInt(), fileSize = resource.size.toInt(),
fileName = image.md5.toUHexString("") + "." + image.formatName, fileName = resource.md5.toUHexString("") + "." + resource.formatName,
imgOriginal = 1 imgOriginal = 1
) )
).sendAndExpect<LongConn.OffPicUp.Response>() ).sendAndExpect<LongConn.OffPicUp.Response>()
} }
when (response) { return when (response) {
is LongConn.OffPicUp.Response.FileExists -> net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId) is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId)
.also { .also {
ImageUploadEvent.Succeed(this, image, it).broadcast() ImageUploadEvent.Succeed(this, resource, it).broadcast()
} }
is LongConn.OffPicUp.Response.RequireUpload -> { is LongConn.OffPicUp.Response.RequireUpload -> {
bot.network.logger.verbose { bot.network.logger.verbose {
"[Http] Uploading friend image, size=${image.input.size.sizeToString()}" "[Http] Uploading friend image, size=${resource.size.sizeToString()}"
} }
val time = measureTime { val time = measureTime {
@ -74,13 +72,13 @@ internal abstract class AbstractUser(
"0x6ff0070", "0x6ff0070",
bot.id, bot.id,
null, null,
imageInput = image.input, imageInput = resource,
uKeyHex = response.uKey.toUHexString("") uKeyHex = response.uKey.toUHexString("")
) )
} }
bot.network.logger.verbose { 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 { OfflineFriendImage(response.resourceId).also {
ImageUploadEvent.Succeed(this, image, it).broadcast() ImageUploadEvent.Succeed(this, resource, it).broadcast()
} }
} }
is LongConn.OffPicUp.Response.Failed -> { is LongConn.OffPicUp.Response.Failed -> {
ImageUploadEvent.Failed(this, image, -1, response.message).broadcast() ImageUploadEvent.Failed(this, resource, -1, response.message).broadcast()
error(response.message) 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.action.MemberNudge
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message 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 net.mamoe.mirai.utils.MemberDeprecatedApi
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -39,7 +39,7 @@ internal class AnonymousMemberImpl(
override val remark: String get() = memberInfo.remark override val remark: String get() = memberInfo.remark
override fun nudge(): MemberNudge = notSupported("Nudge") 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() { 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.OfflineGroupImage
import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
import net.mamoe.mirai.internal.message.firstIsInstanceOrNull 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.highway.HighwayHelper
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg 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.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache 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.estimateLength
import net.mamoe.mirai.internal.utils.toUHexString
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
@ -228,53 +227,47 @@ internal class GroupImpl(
return result.getOrThrow() return result.getOrThrow()
} }
@Suppress("DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
override suspend fun uploadImage(image: ExternalImage): Image = try { override suspend fun uploadImage(resource: ExternalResource): Image {
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) { if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
image.input.init(bot.configuration.fileCacheStrategy)
}
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
} }
bot.network.run { bot.network.run<QQAndroidBotNetworkHandler, Image> {
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp( val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
bot.client, bot.client,
uin = bot.id, uin = bot.id,
groupCode = id, groupCode = id,
md5 = image.md5, md5 = resource.md5,
size = image.input.size.toInt() size = resource.size.toInt()
).sendAndExpect() ).sendAndExpect()
@Suppress("UNCHECKED_CAST") // bug @Suppress("UNCHECKED_CAST") // bug
when (response) { when (response) {
is ImgStore.GroupPicUp.Response.Failed -> { 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() if (response.message == "over file size max") throw OverFileSizeMaxException()
error("upload group image failed with reason ${response.message}") error("upload group image failed with reason ${response.message}")
} }
is ImgStore.GroupPicUp.Response.FileExists -> { is ImgStore.GroupPicUp.Response.FileExists -> {
val resourceId = image.calculateImageResourceId() val resourceId = resource.calculateResourceId()
return OfflineGroupImage(imageId = resourceId) 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 -> { is ImgStore.GroupPicUp.Response.RequireUpload -> {
HighwayHelper.uploadImageToServers( HighwayHelper.uploadImageToServers(
bot, bot,
response.uploadIpList.zip(response.uploadPortList), response.uploadIpList.zip(response.uploadPortList),
response.uKey, response.uKey,
image.input, resource,
kind = "group image", kind = "group image",
commandId = 2 commandId = 2
) )
val resourceId = image.calculateImageResourceId() val resourceId = resource.calculateResourceId()
return OfflineGroupImage(imageId = resourceId) 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) { if (content.size > 1048576) {
throw OverFileSizeMaxException() throw OverFileSizeMaxException()
} }
val md5 = MiraiPlatformUtils.md5(content) val md5 = content.md5()
val codec = with(content.copyOfRange(0, 10).toUHexString("")) { val codec = with(content.copyOfRange(0, 10).toUHexString("")) {
when { when {
startsWith("2321414D52") -> 0 // amr 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.MessageReceipt
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalResource
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
internal class OtherClientImpl( internal class OtherClientImpl(
@ -27,7 +27,7 @@ internal class OtherClientImpl(
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.") 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.") 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.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.safeCast 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.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@ -58,7 +60,7 @@ internal fun MessageChain.toRichTextElems(
fun transformOneMessage(it: Message) { fun transformOneMessage(it: Message) {
if (it is RichMessage) { if (it is RichMessage) {
val content = MiraiPlatformUtils.zip(it.content.toByteArray()) val content = it.content.toByteArray().zip()
when (it) { when (it) {
is ForwardMessageInternal -> { is ForwardMessageInternal -> {
elements.add( elements.add(
@ -422,7 +424,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, botId:
{ "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) { { "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) {
when (element.lightApp.data[0].toInt()) { when (element.lightApp.data[0].toInt()) {
0 -> element.lightApp.data.encodeToString(offset = 1) 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]}") 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() }) { val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {
when (element.richMsg.template1[0].toInt()) { when (element.richMsg.template1[0].toInt()) {
0 -> element.richMsg.template1.encodeToString(offset = 1) 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]}") 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.FRIEND_IMAGE_ID_REGEX_2
import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
import net.mamoe.mirai.message.data.md5 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.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 class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace internal val delegate: ImMsgBody.CustomFace
) : @Suppress("DEPRECATION") ) : @Suppress("DEPRECATION")
OnlineGroupImage() { OnlineGroupImage() {
override val imageId: String = ExternalImage.generateImageId( override val imageId: String = generateImageId(
delegate.md5, delegate.md5,
delegate.filePath.substringAfterLast('.') delegate.filePath.substringAfterLast('.')
).takeIf { ).takeIf {
GROUP_IMAGE_ID_REGEX.matches(it) GROUP_IMAGE_ID_REGEX.matches(it)
} ?: ExternalImage.generateImageId(delegate.md5) } ?: generateImageId(delegate.md5)
override val originUrl: String override val originUrl: String
get() = if (delegate.origUrl.isBlank()) { get() = if (delegate.origUrl.isBlank()) {
@ -145,14 +157,12 @@ internal interface SuspendDeferredOriginUrlAware : Image {
@MiraiExperimentalApi("Will be renamed to OfflineImage on 1.2.0") @MiraiExperimentalApi("Will be renamed to OfflineImage on 1.2.0")
@Suppress("DEPRECATION_ERROR") @Suppress("DEPRECATION_ERROR")
internal class ExperimentalDeferredImage internal constructor( 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 { ) : AbstractImage(), SuspendDeferredOriginUrlAware {
override suspend fun getUrl(bot: Bot): String { override suspend fun getUrl(bot: Bot): String {
TODO() TODO()
} }
override val imageId: String = externalImage.calculateResourceId()
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
override val imageId: String = externalImage.calculateImageResourceId()
} }
@Suppress("EXPOSED_SUPER_INTERFACE") @Suppress("EXPOSED_SUPER_INTERFACE")

View File

@ -38,7 +38,7 @@ internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddre
*/ */
@Suppress("RemoveRedundantQualifierName") // bug @Suppress("RemoveRedundantQualifierName") // bug
private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray = private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(androidId + macAddress) (androidId + macAddress).md5()
/** /**
* 生成长度为 [length], 元素为随机 `0..255` [ByteArray] * 生成长度为 [length], 元素为随机 `0..255` [ByteArray]
@ -317,7 +317,7 @@ internal open class QQAndroidClient(
@Suppress("RemoveRedundantQualifierName") // bug @Suppress("RemoveRedundantQualifierName") // bug
internal fun generateTgtgtKey(guid: ByteArray): ByteArray = internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(getRandomByteArray(16) + guid) (getRandomByteArray(16) + guid).md5()
internal class ReserveUinInfo( internal class ReserveUinInfo(

View File

@ -7,8 +7,6 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package net.mamoe.mirai.internal.network.highway package net.mamoe.mirai.internal.network.highway
import io.ktor.client.* import io.ktor.client.*
@ -16,21 +14,25 @@ import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.io.core.discardExact import kotlinx.io.core.*
import kotlinx.io.core.use
import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead 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.readProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.io.withUse import net.mamoe.mirai.internal.utils.io.withUse
import net.mamoe.mirai.utils.internal.ReusableInput import net.mamoe.mirai.internal.utils.toIpV4AddressString
import net.mamoe.mirai.utils.verbose import net.mamoe.mirai.utils.*
import java.io.InputStream
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTime import kotlin.time.measureTime
@ -41,7 +43,7 @@ internal suspend fun HttpClient.postImage(
htcmd: String, htcmd: String,
uin: Long, uin: Long,
groupcode: Long?, groupcode: Long?,
imageInput: ReusableInput, imageInput: ExternalResource,
uKeyHex: String uKeyHex: String
): Boolean = post<HttpStatusCode> { ): Boolean = post<HttpStatusCode> {
url { url {
@ -65,12 +67,10 @@ internal suspend fun HttpClient.postImage(
body = object : OutgoingContent.WriteChannelContent() { body = object : OutgoingContent.WriteChannelContent() {
override val contentType: ContentType = ContentType.Image.Any 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) { override suspend fun writeTo(channel: ByteWriteChannel) {
imageInput.writeTo(channel) imageInput.inputStream().withUse { copyTo(channel) }
} }
} }
} == HttpStatusCode.OK } == HttpStatusCode.OK
@ -82,7 +82,7 @@ internal object HighwayHelper {
bot: QQAndroidBot, bot: QQAndroidBot,
servers: List<Pair<Int, Int>>, servers: List<Pair<Int, Int>>,
uKey: ByteArray, uKey: ByteArray,
image: ReusableInput, image: ExternalResource,
kind: String, kind: String,
commandId: Int commandId: Int
) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId) ) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId)
@ -94,11 +94,11 @@ internal object HighwayHelper {
servers: List<Pair<Int, Int>>, servers: List<Pair<Int, Int>>,
uKey: ByteArray, uKey: ByteArray,
md5: ByteArray, md5: ByteArray,
input: ReusableInput, input: ExternalResource,
kind: String, kind: String,
commandId: Int commandId: Int
) = servers.retryWithServers( ) = servers.retryWithServers(
(input.size * 1000 / 1024 / 10).coerceAtLeast(5000), (input.size * 1000 / 1024 / 10).coerceAtLeast(5000).toLong(),
onFail = { onFail = {
throw IllegalStateException("cannot upload $kind, failed on all servers.", it) throw IllegalStateException("cannot upload $kind, failed on all servers.", it)
} }
@ -131,7 +131,7 @@ internal object HighwayHelper {
serverIp: String, serverIp: String,
serverPort: Int, serverPort: Int,
ticket: ByteArray, ticket: ByteArray,
imageInput: ReusableInput, imageInput: ExternalResource,
fileMd5: ByteArray, fileMd5: ByteArray,
commandId: Int // group=2, friend=1 commandId: Int // group=2, friend=1
) { ) {
@ -157,18 +157,16 @@ internal object HighwayHelper {
ticket = ticket, ticket = ticket,
data = imageInput, data = imageInput,
fileMd5 = fileMd5 fileMd5 = fileMd5
).withUse { ).useAll {
flow.collect { socket.send(it)
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
//0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
socket.read().withUse { socket.read().withUse {
discardExact(1) discardExact(1)
val headLength = readInt() val headLength = readInt()
discardExact(4) discardExact(4)
val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength) val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" } check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" }
}
} }
} }
} }
@ -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( internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
timeoutMillis: Long, timeoutMillis: Long,
@ -249,6 +328,7 @@ internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
onFail(exception) onFail(exception)
} }
internal fun Int.sizeToString() = this.toLong().sizeToString()
internal fun Long.sizeToString(): String { internal fun Long.sizeToString(): String {
return if (this < 1024) { return if (this < 1024) {
"$this B" "$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.StatSvc
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.internal.network.readUShortLVByteArray 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.TEA
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
import net.mamoe.mirai.internal.utils.io.readPacketExact import net.mamoe.mirai.internal.utils.io.readPacketExact
import net.mamoe.mirai.internal.utils.io.readString import net.mamoe.mirai.internal.utils.io.readString
import net.mamoe.mirai.internal.utils.io.useBytes import net.mamoe.mirai.internal.utils.io.useBytes
import net.mamoe.mirai.internal.utils.io.withUse 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.* import net.mamoe.mirai.utils.*
internal sealed class PacketFactory<TPacket : Packet?> { internal sealed class PacketFactory<TPacket : Packet?> {
@ -348,7 +350,7 @@ internal object KnownPacketFactories {
1 -> { 1 -> {
input.discardExact(4) input.discardExact(4)
input.useBytes { data, length -> input.useBytes { data, length ->
MiraiPlatformUtils.unzip(data, 0, length).let { data.unzip(0, length).let {
val size = it.toInt() val size = it.toInt()
if (size == it.size || size == it.size + 4) { if (size == it.size || size == it.size + 4) {
it.toReadPacket(offset = 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.toByteArray
import kotlinx.io.core.writeFully import kotlinx.io.core.writeFully
import net.mamoe.mirai.internal.network.protocol.LoginType 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.NetworkType
import net.mamoe.mirai.internal.utils.io.* import net.mamoe.mirai.internal.utils.io.*
import net.mamoe.mirai.internal.utils.toByteArray import net.mamoe.mirai.internal.utils.toByteArray
import net.mamoe.mirai.utils.currentTimeMillis import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.mirai.utils.md5
import kotlin.random.Random import kotlin.random.Random
/** /**
@ -101,8 +101,10 @@ internal fun BytePacketBuilder.t106(
guid?.requireSize(16) guid?.requireSize(16)
writeShortLVPacket { writeShortLVPacket {
encryptAndWrite(MiraiPlatformUtils.md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt() encryptAndWrite(
.toByteArray())) { (passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt()
.toByteArray()).md5()
) {
writeShort(4)//TGTGTVer writeShort(4)//TGTGTVer
writeInt(Random.nextInt()) writeInt(Random.nextInt())
writeInt(ssoVersion)//ssoVer writeInt(ssoVersion)//ssoVer
@ -335,7 +337,7 @@ internal fun BytePacketBuilder.t109(
) { ) {
writeShort(0x109) writeShort(0x109)
writeShortLVPacket { writeShortLVPacket {
writeFully(MiraiPlatformUtils.md5(androidId)) writeFully(androidId.md5())
} shouldEqualsTo 16 } shouldEqualsTo 16
} }
@ -571,7 +573,7 @@ internal fun BytePacketBuilder.t187(
) { ) {
writeShort(0x187) writeShort(0x187)
writeShortLVPacket { writeShortLVPacket {
writeFully(MiraiPlatformUtils.md5(macAddress)) // may be md5 writeFully(macAddress.md5()) // may be md5
} }
} }
@ -581,7 +583,7 @@ internal fun BytePacketBuilder.t188(
) { ) {
writeShort(0x188) writeShort(0x188)
writeShortLVPacket { writeShortLVPacket {
writeFully(MiraiPlatformUtils.md5(androidId)) writeFully(androidId.md5())
} shouldEqualsTo 16 } 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.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket 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._miraiContentToString
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf 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.toByteArray
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
import net.mamoe.mirai.message.data.ForwardMessage import net.mamoe.mirai.message.data.ForwardMessage
import net.mamoe.mirai.message.data.asMessageChain import net.mamoe.mirai.message.data.asMessageChain
import net.mamoe.mirai.utils.gzip
import net.mamoe.mirai.utils.md5
internal class MessageValidationData( internal class MessageValidationData(
val data: ByteArray, val data: ByteArray,
val md5: ByteArray = MiraiPlatformUtils.md5(data) val md5: ByteArray = data.md5()
) { ) {
override fun toString(): String { override fun toString(): String {
return "MessageValidationData(data=<size=${data.size}>, md5=${md5.contentToString()})" 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()) val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
return MessageValidationData(MiraiPlatformUtils.gzip(bytes)) return MessageValidationData(bytes.gzip())
} }
internal class MultiMsg { internal class MultiMsg {

View File

@ -10,7 +10,6 @@
package net.mamoe.mirai.internal.network.protocol.packet.login package net.mamoe.mirai.internal.network.protocol.packet.login
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.use
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.event.AbstractEvent 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.IncomingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket 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.hexToBytes
import net.mamoe.mirai.internal.utils.io.ProtoBuf import net.mamoe.mirai.internal.utils.io.ProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.* import net.mamoe.mirai.internal.utils.io.serialization.*
import net.mamoe.mirai.internal.utils.io.withUse import net.mamoe.mirai.internal.utils.io.withUse
import net.mamoe.mirai.internal.utils.toReadPacket import net.mamoe.mirai.internal.utils.toReadPacket
import net.mamoe.mirai.utils.ByteArrayPool
import net.mamoe.mirai.utils.verbose import net.mamoe.mirai.utils.verbose
import net.mamoe.mirai.internal.network.protocol.data.jce.PushReq as PushReqJceStruct 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 package net.mamoe.mirai.internal.network.protocol.packet.login
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.io.core.ByteReadPacket 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.Oidb0x769
import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline
import net.mamoe.mirai.internal.network.protocol.packet.* 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.internal.utils.io.serialization.*
import net.mamoe.mirai.utils.currentTimeMillis 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") @Suppress("EnumEntryName", "unused")
internal enum class RegPushReason { internal enum class RegPushReason {
@ -157,7 +161,7 @@ internal class StatSvc {
strDevType = client.device.model.encodeToString(), strDevType = client.device.model.encodeToString(),
strOSVer = client.device.version.release.encodeToString(), strOSVer = client.device.version.release.encodeToString(),
uOldSSOIp = 0, uOldSSOIp = 0,
uNewSSOIp = MiraiPlatformUtils.localIpAddress().runCatching { ipToLong() } uNewSSOIp = localIpAddress().runCatching { ipToLong() }
.getOrElse { "192.168.1.123".ipToLong() }, .getOrElse { "192.168.1.123".ipToLong() },
strVendorName = "MIUI", strVendorName = "MIUI",
strVendorOSName = "?ONEPLUS A5000_23_17", 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.currentTimeSeconds
import net.mamoe.mirai.utils.error import net.mamoe.mirai.utils.error
import net.mamoe.mirai.utils.generateDeviceInfoData import net.mamoe.mirai.utils.generateDeviceInfoData
import net.mamoe.mirai.utils.md5
internal class WtLogin { internal class WtLogin {
/** /**
@ -80,7 +81,7 @@ internal class WtLogin {
t8(2052) t8(2052)
t104(client.t104) t104(client.t104)
t116(client.miscBitMap, client.subSigMap) 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.charsets.Charsets
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.String 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 java.util.*
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmSynthetic
@JvmOverloads @JvmOverloads
@ -107,5 +104,5 @@ internal inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
contract { contract {
callsInPlace(t, InvocationKind.EXACTLY_ONCE) 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 package net.mamoe.mirai.internal.utils.crypto
import kotlinx.io.core.ByteReadPacket 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.toByteArray
import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.internal.utils.toUHexString
import net.mamoe.mirai.utils.ByteArrayPool
import kotlin.experimental.and import kotlin.experimental.and
import kotlin.experimental.xor import kotlin.experimental.xor
import kotlin.random.Random 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.Charset
import kotlinx.io.charsets.Charsets import kotlinx.io.charsets.Charsets
import kotlinx.io.core.* import kotlinx.io.core.*
import net.mamoe.mirai.internal.utils.ByteArrayPool
import net.mamoe.mirai.internal.utils.toReadPacket import net.mamoe.mirai.internal.utils.toReadPacket
import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.internal.utils.toUHexString
import net.mamoe.mirai.utils.ByteArrayPool
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
internal inline fun <R> ByteReadPacket.useBytes( internal inline fun <R> ByteReadPacket.useBytes(

View File

@ -9,6 +9,10 @@
package net.mamoe.mirai.internal.utils package net.mamoe.mirai.internal.utils
import kotlinx.io.core.toByteArray 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.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -16,11 +20,11 @@ internal class PlatformUtilsTest {
@Test @Test
fun testZip() { fun testZip() {
assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString()) assertEquals("test", "test".toByteArray().zip().unzip().encodeToString())
} }
@Test @Test
fun testGZip() { 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.ByteReadPacket
import kotlinx.io.core.Input import kotlinx.io.core.Input
import kotlinx.io.core.readAvailable import kotlinx.io.core.readAvailable
import kotlinx.io.pool.useInstance import kotlinx.io.core.use
import net.mamoe.mirai.internal.utils.ByteArrayPool
import net.mamoe.mirai.internal.utils.toReadPacket import net.mamoe.mirai.internal.utils.toReadPacket
import net.mamoe.mirai.internal.utils.toUHexString import net.mamoe.mirai.internal.utils.toUHexString
import net.mamoe.mirai.utils.ByteArrayPool
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.MiraiLoggerWithSwitch import net.mamoe.mirai.utils.MiraiLoggerWithSwitch
import net.mamoe.mirai.utils.withSwitch import net.mamoe.mirai.utils.withSwitch

View File

@ -9,7 +9,7 @@
package net.mamoe.mirai.internal.utils.crypto 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 org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.* import java.security.*
import java.security.spec.ECGenParameterSpec 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") val instance = KeyAgreement.getInstance("ECDH", "BC")
instance.init(privateKey) instance.init(privateKey)
instance.doPhase(publicKey, true) instance.doPhase(publicKey, true)
return MiraiPlatformUtils.md5(instance.generateSecret()) return instance.generateSecret().md5()
} }
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {