mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-03 15:10:14 +08:00
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:
parent
c3bbabc274
commit
bfda72e58f
@ -23,6 +23,7 @@ import net.mamoe.mirai.message.action.Nudge
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.message.data.Image.Key.queryUrl
|
||||
import net.mamoe.mirai.message.data.MessageSource.Key.recall
|
||||
import net.mamoe.mirai.utils.FileCacheStrategy
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
|
||||
@ -35,7 +36,7 @@ public val Mirai: IMirai by lazy { findMiraiInstance() }
|
||||
/**
|
||||
* Mirai API 接口.
|
||||
*
|
||||
* @see Mirai
|
||||
* @see Mirai 获取实例
|
||||
*/
|
||||
public interface IMirai : LowLevelApiAccessor {
|
||||
/**
|
||||
@ -47,6 +48,13 @@ public interface IMirai : LowLevelApiAccessor {
|
||||
@MiraiExperimentalApi
|
||||
public val BotFactory: BotFactory
|
||||
|
||||
/**
|
||||
* Mirai 全局使用的 [FileCacheStrategy].
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
@MiraiExperimentalApi
|
||||
public var FileCacheStrategy: FileCacheStrategy
|
||||
|
||||
/**
|
||||
* 使用 groupCode 计算 groupUin. 这两个值仅在 mirai 内部协议区分, 一般人使用时无需在意.
|
||||
*/
|
||||
|
@ -20,9 +20,9 @@ import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.MessageReceipt.Companion.quote
|
||||
import net.mamoe.mirai.message.MessageReceipt.Companion.recall
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.OverFileSizeMaxException
|
||||
import net.mamoe.mirai.utils.WeakRefProperty
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], 和 [群][Group].
|
||||
@ -70,21 +70,51 @@ public interface Contact : ContactOrBot, CoroutineScope {
|
||||
/**
|
||||
* 上传一个图片以备发送.
|
||||
*
|
||||
* 无论上传是否成功都不会关闭 [resource].
|
||||
*
|
||||
* @see Image 查看有关图片的更多信息, 如上传图片
|
||||
*
|
||||
* @see BeforeImageUploadEvent 图片发送前事件, 可拦截.
|
||||
* @see ImageUploadEvent 图片发送完成事件, 不可拦截.
|
||||
*
|
||||
* @see ExternalResource
|
||||
*
|
||||
* @throws EventCancelledException 当发送消息事件被取消时抛出
|
||||
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, 但 mirai 限制的大小为 30 MB)
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public suspend fun uploadImage(image: ExternalImage): Image
|
||||
public suspend fun uploadImage(resource: ExternalResource): Image
|
||||
|
||||
/**
|
||||
* @return "Friend($id)" or "Group($id)" or "Member($id)"
|
||||
*/
|
||||
public override fun toString(): String
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
|
||||
*
|
||||
* 注意:此函数不会关闭 [imageStream]
|
||||
*
|
||||
* @throws OverFileSizeMaxException
|
||||
* @see FileCacheStrategy
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
@JvmStatic
|
||||
@JvmBlockingBridge
|
||||
public suspend fun <C : Contact> C.sendImage(imageStream: InputStream): MessageReceipt<C> =
|
||||
imageStream.sendAsImageTo(this)
|
||||
|
||||
/**
|
||||
* 将文件作为图片发送到指定联系人
|
||||
* @throws OverFileSizeMaxException
|
||||
* @see FileCacheStrategy
|
||||
*/
|
||||
@Throws(OverFileSizeMaxException::class)
|
||||
@JvmStatic
|
||||
@JvmBlockingBridge
|
||||
public suspend fun <C : Contact> C.sendImage(file: File): MessageReceipt<C> = file.sendAsImageTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,7 +18,6 @@ import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.MessageReceipt.Companion.recall
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.OverFileSizeMaxException
|
||||
import net.mamoe.mirai.utils.PlannedRemoval
|
||||
@ -162,21 +161,6 @@ public interface Group : Contact, CoroutineScope {
|
||||
public override suspend fun sendMessage(message: String): MessageReceipt<Group> =
|
||||
this.sendMessage(message.toPlainText())
|
||||
|
||||
|
||||
/**
|
||||
* 上传一个图片以备发送.
|
||||
*
|
||||
* @see Image 查看有关图片的更多信息, 如上传图片
|
||||
*
|
||||
* @see BeforeImageUploadEvent 图片上传前事件, cancellable
|
||||
* @see ImageUploadEvent 图片上传完成事件
|
||||
*
|
||||
* @throws EventCancelledException 当发送消息事件被取消
|
||||
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public override suspend fun uploadImage(image: ExternalImage): Image
|
||||
|
||||
/**
|
||||
* 上传一个语音消息以备发送.
|
||||
* 请手动关闭输入流
|
||||
|
@ -18,7 +18,7 @@ import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PAD
|
||||
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol.ANDROID_PHONE
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
|
||||
@ -42,7 +42,7 @@ public interface OtherClient : Contact {
|
||||
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
|
||||
}
|
||||
|
||||
override suspend fun uploadImage(image: ExternalImage): Image {
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image {
|
||||
throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.")
|
||||
}
|
||||
}
|
||||
|
@ -14,18 +14,17 @@ package net.mamoe.mirai.contact
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.kjbb.JvmBlockingBridge
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.event.events.EventCancelledException
|
||||
import net.mamoe.mirai.event.events.UserMessagePostSendEvent
|
||||
import net.mamoe.mirai.event.events.UserMessagePreSendEvent
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.MessageReceipt.Companion.recall
|
||||
import net.mamoe.mirai.message.action.Nudge
|
||||
import net.mamoe.mirai.message.action.UserNudge
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.isContentEmpty
|
||||
import net.mamoe.mirai.message.data.toPlainText
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.OverFileSizeMaxException
|
||||
|
||||
/**
|
||||
* 代表一个 **用户**.
|
||||
@ -92,20 +91,6 @@ public interface User : Contact, UserOrBot, CoroutineScope {
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public override fun nudge(): UserNudge
|
||||
|
||||
/**
|
||||
* 上传一个图片以备发送.
|
||||
*
|
||||
* @see Image 查看有关图片的更多信息, 如上传图片
|
||||
*
|
||||
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
|
||||
* @see ImageUploadEvent 图片发送完成事件
|
||||
*
|
||||
* @throws EventCancelledException 当发送消息事件被取消
|
||||
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public override suspend fun uploadImage(image: ExternalImage): Image
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,6 +16,7 @@ package net.mamoe.mirai.event.events
|
||||
import net.mamoe.kjbb.JvmBlockingBridge
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.contact.Contact.Companion.sendImage
|
||||
import net.mamoe.mirai.event.*
|
||||
import net.mamoe.mirai.event.events.ImageUploadEvent.Failed
|
||||
import net.mamoe.mirai.event.events.ImageUploadEvent.Succeed
|
||||
@ -26,7 +27,8 @@ import net.mamoe.mirai.message.data.Image.Key.queryUrl
|
||||
import net.mamoe.mirai.message.data.MessageSource.Key.quote
|
||||
import net.mamoe.mirai.message.isContextIdenticalWith
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.awt.image.BufferedImage
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import kotlin.internal.InlineOnly
|
||||
@ -415,7 +417,7 @@ public val MessageRecallEvent.isByBot: Boolean
|
||||
*/
|
||||
public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
|
||||
public val target: Contact,
|
||||
public val source: ExternalImage
|
||||
public val source: ExternalResource
|
||||
) : BotEvent, BotActiveEvent, AbstractEvent(), CancellableEvent {
|
||||
public override val bot: Bot
|
||||
get() = target.bot
|
||||
@ -434,19 +436,19 @@ public data class BeforeImageUploadEvent @MiraiInternalApi constructor(
|
||||
*/
|
||||
public sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractEvent() {
|
||||
public abstract val target: Contact
|
||||
public abstract val source: ExternalImage
|
||||
public abstract val source: ExternalResource
|
||||
public override val bot: Bot
|
||||
get() = target.bot
|
||||
|
||||
public data class Succeed @MiraiInternalApi constructor(
|
||||
override val target: Contact,
|
||||
override val source: ExternalImage,
|
||||
override val source: ExternalResource,
|
||||
val image: Image
|
||||
) : ImageUploadEvent()
|
||||
|
||||
public data class Failed @MiraiInternalApi constructor(
|
||||
override val target: Contact,
|
||||
override val source: ExternalImage,
|
||||
override val source: ExternalResource,
|
||||
val errno: Int,
|
||||
val message: String
|
||||
) : ImageUploadEvent()
|
||||
@ -631,9 +633,9 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
|
||||
public override suspend fun reply(plain: String): MessageReceipt<Contact> =
|
||||
subject.sendMessage(PlainText(plain).asMessageChain())
|
||||
|
||||
public override suspend fun ExternalImage.upload(): Image = this.upload(subject)
|
||||
public override suspend fun ExternalResource.uploadAsImage(): Image = this.uploadAsImage(subject)
|
||||
|
||||
public override suspend fun ExternalImage.send(): MessageReceipt<Contact> = this.sendTo(subject)
|
||||
public override suspend fun ExternalResource.sendAsImage(): MessageReceipt<Contact> = this.sendAsImageTo(subject)
|
||||
|
||||
public override suspend fun Image.send(): MessageReceipt<Contact> = this.sendTo(subject)
|
||||
|
||||
@ -667,25 +669,21 @@ public abstract class AbstractMessageEvent : MessageEvent, AbstractEvent() {
|
||||
|
||||
// region 上传图片
|
||||
|
||||
public override suspend fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)
|
||||
public override suspend fun uploadImage(image: InputStream): Image = subject.uploadImage(image)
|
||||
public override suspend fun uploadImage(image: File): Image = subject.uploadImage(image)
|
||||
// endregion
|
||||
|
||||
// region 发送图片
|
||||
public override suspend fun sendImage(image: BufferedImage): MessageReceipt<Contact> = subject.sendImage(image)
|
||||
public override suspend fun sendImage(image: InputStream): MessageReceipt<Contact> = subject.sendImage(image)
|
||||
public override suspend fun sendImage(image: File): MessageReceipt<Contact> = subject.sendImage(image)
|
||||
// endregion
|
||||
|
||||
// region 上传图片 (扩展)
|
||||
public override suspend fun BufferedImage.upload(): Image = upload(subject)
|
||||
public override suspend fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
|
||||
public override suspend fun File.uploadAsImage(): Image = uploadAsImage(subject)
|
||||
// endregion 上传图片 (扩展)
|
||||
|
||||
// region 发送图片 (扩展)
|
||||
public override suspend fun BufferedImage.send(): MessageReceipt<Contact> = sendTo(subject)
|
||||
public override suspend fun InputStream.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
|
||||
public override suspend fun File.sendAsImage(): MessageReceipt<Contact> = sendAsImageTo(subject)
|
||||
// endregion 发送图片 (扩展)
|
||||
@ -773,10 +771,10 @@ public interface MessageEventExtensions<out TSender : User, out TSubject : Conta
|
||||
// endregion
|
||||
|
||||
@JvmSynthetic
|
||||
public suspend fun ExternalImage.upload(): Image
|
||||
public suspend fun ExternalResource.uploadAsImage(): Image
|
||||
|
||||
@JvmSynthetic
|
||||
public suspend fun ExternalImage.send(): MessageReceipt<TSubject>
|
||||
public suspend fun ExternalResource.sendAsImage(): MessageReceipt<TSubject>
|
||||
|
||||
@JvmSynthetic
|
||||
public suspend fun Image.send(): MessageReceipt<TSubject>
|
||||
@ -828,9 +826,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
|
||||
|
||||
// region 上传图片
|
||||
|
||||
@JvmBlockingBridge
|
||||
public suspend fun uploadImage(image: BufferedImage): Image
|
||||
|
||||
@JvmBlockingBridge
|
||||
public suspend fun uploadImage(image: InputStream): Image
|
||||
|
||||
@ -839,9 +834,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
|
||||
// endregion
|
||||
|
||||
// region 发送图片
|
||||
@JvmBlockingBridge
|
||||
public suspend fun sendImage(image: BufferedImage): MessageReceipt<TSubject>
|
||||
|
||||
@JvmBlockingBridge
|
||||
public suspend fun sendImage(image: InputStream): MessageReceipt<TSubject>
|
||||
|
||||
@ -850,9 +842,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
|
||||
// endregion
|
||||
|
||||
// region 上传图片 (扩展)
|
||||
@JvmSynthetic
|
||||
public suspend fun BufferedImage.upload(): Image
|
||||
|
||||
@JvmSynthetic
|
||||
public suspend fun InputStream.uploadAsImage(): Image
|
||||
|
||||
@ -861,9 +850,6 @@ public interface MessageEventPlatformExtensions<out TSender : User, out TSubject
|
||||
// endregion 上传图片 (扩展)
|
||||
|
||||
// region 发送图片 (扩展)
|
||||
@JvmSynthetic
|
||||
public suspend fun BufferedImage.send(): MessageReceipt<TSubject>
|
||||
|
||||
@JvmSynthetic
|
||||
public suspend fun InputStream.sendAsImage(): MessageReceipt<TSubject>
|
||||
|
||||
|
@ -19,7 +19,6 @@ import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import net.mamoe.mirai.message.MessageSerializer
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.internal.checkOffsetAndLength
|
||||
|
||||
/**
|
||||
* 自定义消息
|
||||
@ -211,28 +210,3 @@ internal inline fun <T : CustomMessageMetadata> T.customToStringImpl(factory: Cu
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return (factory as CustomMessage.Factory<T>).dump(this)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
@JvmOverloads
|
||||
@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray
|
||||
internal fun ByteArray.toUHexString(
|
||||
separator: String = " ",
|
||||
offset: Int = 0,
|
||||
length: Int = this.size - offset
|
||||
): String {
|
||||
this.checkOffsetAndLength(offset, length)
|
||||
if (length == 0) {
|
||||
return ""
|
||||
}
|
||||
val lastIndex = offset + length
|
||||
return buildString(length * 2) {
|
||||
this@toUHexString.forEachIndexed { index, it ->
|
||||
if (index in offset until lastIndex) {
|
||||
var ret = it.toUByte().toString(16).toUpperCase()
|
||||
if (ret.length == 1) ret = "0$ret"
|
||||
append(ret)
|
||||
if (index < lastIndex - 1) append(separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,10 +112,6 @@ public open class BotConfiguration { // open for Java
|
||||
*/
|
||||
public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub
|
||||
|
||||
/** 缓存策略 */
|
||||
@MiraiExperimentalApi
|
||||
public var fileCacheStrategy: FileCacheStrategy = FileCacheStrategy.PlatformDefault
|
||||
|
||||
/**
|
||||
* Json 序列化器, 使用 'kotlinx.serialization'
|
||||
*/
|
||||
@ -270,7 +266,6 @@ public open class BotConfiguration { // open for Java
|
||||
new.reconnectionRetryTimes = reconnectionRetryTimes
|
||||
new.loginSolver = loginSolver
|
||||
new.protocol = protocol
|
||||
new.fileCacheStrategy = fileCacheStrategy
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import net.mamoe.mirai.utils.internal.getRandomByteArray
|
||||
import net.mamoe.mirai.utils.internal.getRandomIntString
|
||||
import net.mamoe.mirai.utils.internal.getRandomString
|
||||
import net.mamoe.mirai.utils.internal.md5
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@ -78,7 +77,7 @@ public class DeviceInfo(
|
||||
model = "mirai".toByteArray(),
|
||||
bootloader = "unknown".toByteArray(),
|
||||
fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${getRandomIntString(7)}:user/release-keys".toByteArray(),
|
||||
bootId = ExternalImage.generateUUID(getRandomByteArray(16).md5()).toByteArray(),
|
||||
bootId = generateUUID(getRandomByteArray(16).md5()).toByteArray(),
|
||||
procVersion = "Linux version 3.0.31-${getRandomString(8)} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
|
||||
baseBand = byteArrayOf(),
|
||||
version = Version(),
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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))
|
272
mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt
Normal file
272
mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt
Normal 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
|
||||
*/
|
@ -9,108 +9,57 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.utils.internal.asReusableInput
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import net.mamoe.mirai.IMirai
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* 缓存策略.
|
||||
* 资源缓存策略.
|
||||
*
|
||||
* 图片上传时默认使用文件缓存.
|
||||
* 由于上传资源时服务器要求提前给出 MD5 和文件大小等数据, 一些资源如 [InputStream] 需要首先缓存才能使用.
|
||||
*
|
||||
* @see BotConfiguration.fileCacheStrategy 为 [Bot] 指定缓存策略
|
||||
* Mirai 全局都使用 [IMirai.FileCacheStrategy].
|
||||
*
|
||||
* ### 使用 [FileCacheStrategy] 的操作
|
||||
* [ExternalResource.toExternalResource],
|
||||
* [InputStream.uploadAsImage],
|
||||
* [InputStream.sendAsImageTo]
|
||||
*
|
||||
* @see ExternalResource
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public interface FileCacheStrategy {
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [Input]
|
||||
* 立即读取 [input] 所有内容并缓存为 [ExternalResource].
|
||||
*
|
||||
* 注意:
|
||||
* - 此函数不会关闭输入
|
||||
* - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
|
||||
*
|
||||
* @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai"
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
public fun newImageCache(input: Input): ExternalImage
|
||||
@Throws(IOException::class)
|
||||
public fun newCache(input: InputStream, formatName: String? = null): ExternalResource
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此函数应 close 这个 [InputStream]
|
||||
* 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai".
|
||||
*
|
||||
* 注意:
|
||||
* - 此函数不会关闭输入
|
||||
* - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]).
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
public fun newImageCache(input: InputStream): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此 [input] 的内容应是不变的.
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
public fun newImageCache(input: ByteArray): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
* 此 [input] 的内容应是不变的.
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
public fun newImageCache(input: BufferedImage, format: String = "png"): ExternalImage
|
||||
|
||||
/**
|
||||
* 将 [input] 缓存为 [ExternalImage].
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
public fun newImageCache(input: URL): ExternalImage
|
||||
|
||||
/**
|
||||
* 默认的缓存方案, 使用系统临时文件夹存储.
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public object PlatformDefault : FileCacheStrategy by TempCache(null)
|
||||
@Throws(IOException::class)
|
||||
public fun newCache(input: InputStream): ExternalResource = newCache(input, null)
|
||||
|
||||
/**
|
||||
* 使用内存直接存储所有图片文件.
|
||||
*/
|
||||
public object MemoryCache : FileCacheStrategy {
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: Input): ExternalImage {
|
||||
return newImageCache(input.readBytes())
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: InputStream): ExternalImage {
|
||||
return newImageCache(input.readBytes())
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: ByteArray): ExternalImage {
|
||||
return ExternalImage(input.asReusableInput())
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
|
||||
val out = ByteArrayOutputStream()
|
||||
ImageIO.write(input, format, out)
|
||||
return newImageCache(out.toByteArray())
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override fun newImageCache(input: URL): ExternalImage {
|
||||
val out = ByteArrayOutputStream()
|
||||
input.openConnection().getInputStream().use { it.copyTo(out) }
|
||||
return newImageCache(out.toByteArray())
|
||||
@Throws(IOException::class)
|
||||
override fun newCache(input: InputStream, formatName: String?): ExternalResource {
|
||||
return input.readBytes().toExternalResource(formatName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,87 +73,27 @@ public interface FileCacheStrategy {
|
||||
*/
|
||||
public val directory: File? = null
|
||||
) : FileCacheStrategy {
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: Input): ExternalImage {
|
||||
return ExternalImage(File.createTempFile("tmp", null, directory).apply {
|
||||
private fun createTempFile(): File {
|
||||
return File.createTempFile("tmp", null, directory)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun newCache(input: InputStream, formatName: String?): ExternalResource {
|
||||
return createTempFile().apply {
|
||||
deleteOnExit()
|
||||
input.withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
outputStream().use { out -> input.copyTo(out) }
|
||||
}.toExternalResource(formatName)
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai 从 [IMirai.FileCacheStrategy] 获取.
|
||||
*
|
||||
* @see IMirai.FileCacheStrategy
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: InputStream): ExternalImage {
|
||||
return ExternalImage(File.createTempFile("tmp", null, directory).apply {
|
||||
deleteOnExit()
|
||||
input.withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
@Throws(java.io.IOException::class)
|
||||
override fun newImageCache(input: ByteArray): ExternalImage {
|
||||
return ExternalImage(input.asReusableInput())
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override fun newImageCache(input: BufferedImage, format: String): ExternalImage {
|
||||
val file = File.createTempFile("tmp", null, directory).apply { deleteOnExit() }
|
||||
|
||||
val digest = MessageDigest.getInstance("md5")
|
||||
digest.reset()
|
||||
|
||||
file.outputStream().use { out ->
|
||||
ImageIO.write(input, format, object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
out.write(b)
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray) {
|
||||
out.write(b)
|
||||
digest.update(b)
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
out.write(b, off, len)
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
return ExternalImage(file.asReusableInput(true, digest.digest()))
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override fun newImageCache(input: URL): ExternalImage {
|
||||
return ExternalImage(File.createTempFile("tmp", null, directory).apply {
|
||||
deleteOnExit()
|
||||
input.openConnection().getInputStream().withOut(this.outputStream()) { copyTo(it) }
|
||||
}.asReusableInput(true))
|
||||
}
|
||||
@JvmStatic
|
||||
public val PlatformDefault: FileCacheStrategy = TempCache(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(java.io.IOException::class)
|
||||
internal fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(bufferSize)
|
||||
var bytes = readAvailable(buffer)
|
||||
while (bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
bytes = readAvailable(buffer)
|
||||
}
|
||||
return bytesCopied
|
||||
}
|
||||
|
||||
internal inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return use { output.use { block(this, output) } }
|
||||
}
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
@ -7,60 +7,38 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("MiraiUtils")
|
||||
|
||||
package net.mamoe.mirai.utils.internal
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.io.pool.DefaultPool
|
||||
import kotlinx.io.pool.ObjectPool
|
||||
import java.io.InputStream
|
||||
|
||||
internal expect fun InputStream.md5(): ByteArray
|
||||
internal expect fun ByteArray.md5(offset: Int = 0, length: Int = this.size - offset): ByteArray
|
||||
|
||||
@Suppress("DuplicatedCode") // false positive. `this` is not the same for `List<Byte>` and `ByteArray`
|
||||
internal fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) {
|
||||
require(offset >= 0) { "offset shouldn't be negative: $offset" }
|
||||
require(length >= 0) { "length shouldn't be negative: $length" }
|
||||
require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" }
|
||||
}
|
||||
|
||||
|
||||
internal inline fun InputStream.readInSequence(block: (ByteArray, len: Int) -> Unit) {
|
||||
var read: Int
|
||||
ByteArrayPool.useInstance { buf ->
|
||||
while (this.read(buf).also { read = it } != -1) {
|
||||
block(buf, read)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 缓存 [ByteArray] 实例的 [ObjectPool]
|
||||
*/
|
||||
internal object ByteArrayPool : DefaultPool<ByteArray>(256) {
|
||||
public object ByteArrayPool : DefaultPool<ByteArray>(256) {
|
||||
/**
|
||||
* 每一个 [ByteArray] 的大小
|
||||
*/
|
||||
const val BUFFER_SIZE: Int = 8192 * 8
|
||||
public const val BUFFER_SIZE: Int = 8192 * 8
|
||||
|
||||
override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE)
|
||||
|
||||
override fun clearInstance(instance: ByteArray): ByteArray = instance
|
||||
|
||||
fun checkBufferSize(size: Int) {
|
||||
public fun checkBufferSize(size: Int) {
|
||||
require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" }
|
||||
}
|
||||
|
||||
fun checkBufferSize(size: Long) {
|
||||
public fun checkBufferSize(size: Long) {
|
||||
require(size <= BUFFER_SIZE) { "sizePerPacket is too large. Maximum buffer size=$BUFFER_SIZE" }
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求一个大小至少为 [requestedSize] 的 [ByteArray] 实例.
|
||||
*/ // 不要写为扩展函数. 它需要优先于 kotlinx.io 的扩展函数 resolve
|
||||
inline fun <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
|
||||
public inline fun <R> useInstance(requestedSize: Int = 0, block: (ByteArray) -> R): R {
|
||||
if (requestedSize > BUFFER_SIZE) {
|
||||
return ByteArray(requestedSize).run(block)
|
||||
}
|
68
mirai-core-utils/src/commonMain/kotlin/Bytes.kt
Normal file
68
mirai-core-utils/src/commonMain/kotlin/Bytes.kt
Normal 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})" }
|
||||
}
|
@ -7,14 +7,15 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.utils.internal
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("MiraiUtils")
|
||||
|
||||
import net.mamoe.mirai.utils.FileCacheStrategy
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
internal expect class DeferredReusableInput(input: Any, extraArg: Any?) : ReusableInput {
|
||||
val initialized: Boolean
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
suspend fun init(strategy: FileCacheStrategy)
|
||||
|
||||
}
|
||||
public suspend inline fun <R> runBIO(
|
||||
noinline block: suspend CoroutineScope.() -> R
|
||||
): R = withContext(Dispatchers.IO, block)
|
46
mirai-core-utils/src/commonMain/kotlin/Files.kt
Normal file
46
mirai-core-utils/src/commonMain/kotlin/Files.kt
Normal 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
|
||||
}
|
146
mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt
Normal file
146
mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt
Normal 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) } }
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlin.test.Test
|
@ -10,10 +10,8 @@
|
||||
|
||||
package net.mamoe.mirai.internal
|
||||
|
||||
import kotlinx.io.core.toByteArray
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
import net.mamoe.mirai.utils.md5
|
||||
|
||||
internal data class BotAccount(
|
||||
@JvmSynthetic
|
||||
@ -22,7 +20,7 @@ internal data class BotAccount(
|
||||
@MiraiExperimentalApi
|
||||
val passwordMd5: ByteArray // md5
|
||||
) {
|
||||
constructor(id: Long, passwordPlainText: String) : this(id, MiraiPlatformUtils.md5(passwordPlainText.toByteArray()))
|
||||
constructor(id: Long, passwordPlainText: String) : this(id, passwordPlainText.md5())
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
@ -30,7 +30,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.*
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.internal.utils.encodeToString
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
@ -39,10 +38,8 @@ import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1
|
||||
import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2
|
||||
import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
@ -62,6 +59,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
override val BotFactory: BotFactory
|
||||
get() = BotFactoryImpl
|
||||
|
||||
override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
|
||||
|
||||
@OptIn(LowLevelApi::class)
|
||||
override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
|
||||
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
@ -609,8 +608,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
bot,
|
||||
response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
|
||||
response.proto.msgSig,
|
||||
MiraiPlatformUtils.md5(body),
|
||||
net.mamoe.mirai.utils.internal.asReusableInput0(body), // don't use toLongUnsigned: Overload resolution ambiguity
|
||||
body.md5(),
|
||||
body.toExternalResource(null), // don't use toLongUnsigned: Overload resolution ambiguity
|
||||
"group long message",
|
||||
27
|
||||
)
|
||||
@ -801,7 +800,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
else -> error("Internal error: unsupported image class: ${image::class.simpleName}")
|
||||
}
|
||||
|
||||
@Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
|
||||
if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
|
||||
throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")
|
||||
|
@ -15,14 +15,15 @@ import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BeforeImageUploadEvent
|
||||
import net.mamoe.mirai.event.events.EventCancelledException
|
||||
import net.mamoe.mirai.event.events.ImageUploadEvent
|
||||
import net.mamoe.mirai.internal.message.OfflineFriendImage
|
||||
import net.mamoe.mirai.internal.network.highway.postImage
|
||||
import net.mamoe.mirai.internal.network.highway.sizeToString
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.utils.verbose
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.roundToInt
|
||||
@ -38,11 +39,8 @@ internal abstract class AbstractUser(
|
||||
final override val remark: String = friendInfo.remark
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
override suspend fun uploadImage(image: ExternalImage): Image = try {
|
||||
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
|
||||
image.input.init(bot.configuration.fileCacheStrategy)
|
||||
}
|
||||
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image {
|
||||
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
val response = bot.network.run {
|
||||
@ -51,22 +49,22 @@ internal abstract class AbstractUser(
|
||||
srcUin = bot.id.toInt(),
|
||||
dstUin = id.toInt(),
|
||||
fileId = 0,
|
||||
fileMd5 = image.md5,
|
||||
fileSize = image.input.size.toInt(),
|
||||
fileName = image.md5.toUHexString("") + "." + image.formatName,
|
||||
fileMd5 = resource.md5,
|
||||
fileSize = resource.size.toInt(),
|
||||
fileName = resource.md5.toUHexString("") + "." + resource.formatName,
|
||||
imgOriginal = 1
|
||||
)
|
||||
).sendAndExpect<LongConn.OffPicUp.Response>()
|
||||
}
|
||||
|
||||
when (response) {
|
||||
is LongConn.OffPicUp.Response.FileExists -> net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId)
|
||||
return when (response) {
|
||||
is LongConn.OffPicUp.Response.FileExists -> OfflineFriendImage(response.resourceId)
|
||||
.also {
|
||||
ImageUploadEvent.Succeed(this, image, it).broadcast()
|
||||
ImageUploadEvent.Succeed(this, resource, it).broadcast()
|
||||
}
|
||||
is LongConn.OffPicUp.Response.RequireUpload -> {
|
||||
bot.network.logger.verbose {
|
||||
"[Http] Uploading friend image, size=${image.input.size.sizeToString()}"
|
||||
"[Http] Uploading friend image, size=${resource.size.sizeToString()}"
|
||||
}
|
||||
|
||||
val time = measureTime {
|
||||
@ -74,13 +72,13 @@ internal abstract class AbstractUser(
|
||||
"0x6ff0070",
|
||||
bot.id,
|
||||
null,
|
||||
imageInput = image.input,
|
||||
imageInput = resource,
|
||||
uKeyHex = response.uKey.toUHexString("")
|
||||
)
|
||||
}
|
||||
|
||||
bot.network.logger.verbose {
|
||||
"[Http] Uploading friend image: succeed at ${(image.input.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
|
||||
"[Http] Uploading friend image: succeed at ${(resource.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
|
||||
}
|
||||
|
||||
/*
|
||||
@ -94,16 +92,14 @@ internal abstract class AbstractUser(
|
||||
)*/
|
||||
// 为什么不能 ??
|
||||
|
||||
net.mamoe.mirai.internal.message.OfflineFriendImage(response.resourceId).also {
|
||||
ImageUploadEvent.Succeed(this, image, it).broadcast()
|
||||
OfflineFriendImage(response.resourceId).also {
|
||||
ImageUploadEvent.Succeed(this, resource, it).broadcast()
|
||||
}
|
||||
}
|
||||
is LongConn.OffPicUp.Response.Failed -> {
|
||||
ImageUploadEvent.Failed(this, image, -1, response.message).broadcast()
|
||||
ImageUploadEvent.Failed(this, resource, -1, response.message).broadcast()
|
||||
error(response.message)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
image.input.release()
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ import net.mamoe.mirai.internal.MiraiImpl
|
||||
import net.mamoe.mirai.message.action.MemberNudge
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.MemberDeprecatedApi
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@ -39,7 +39,7 @@ internal class AnonymousMemberImpl(
|
||||
override val remark: String get() = memberInfo.remark
|
||||
|
||||
override fun nudge(): MemberNudge = notSupported("Nudge")
|
||||
override suspend fun uploadImage(image: ExternalImage): Image = notSupported("Upload image to")
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image = notSupported("Upload image to")
|
||||
override suspend fun unmute() {
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import net.mamoe.mirai.internal.message.MessageSourceToGroupImpl
|
||||
import net.mamoe.mirai.internal.message.OfflineGroupImage
|
||||
import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
|
||||
import net.mamoe.mirai.internal.message.firstIsInstanceOrNull
|
||||
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
|
||||
import net.mamoe.mirai.internal.network.highway.HighwayHelper
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
|
||||
@ -31,9 +32,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGro
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
|
||||
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.internal.utils.estimateLength
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
@ -228,53 +227,47 @@ internal class GroupImpl(
|
||||
return result.getOrThrow()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun uploadImage(image: ExternalImage): Image = try {
|
||||
if (image.input is net.mamoe.mirai.utils.internal.DeferredReusableInput) {
|
||||
image.input.init(bot.configuration.fileCacheStrategy)
|
||||
}
|
||||
if (BeforeImageUploadEvent(this, image).broadcast().isCancelled) {
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image {
|
||||
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
|
||||
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
|
||||
}
|
||||
bot.network.run {
|
||||
bot.network.run<QQAndroidBotNetworkHandler, Image> {
|
||||
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
|
||||
bot.client,
|
||||
uin = bot.id,
|
||||
groupCode = id,
|
||||
md5 = image.md5,
|
||||
size = image.input.size.toInt()
|
||||
md5 = resource.md5,
|
||||
size = resource.size.toInt()
|
||||
).sendAndExpect()
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // bug
|
||||
when (response) {
|
||||
is ImgStore.GroupPicUp.Response.Failed -> {
|
||||
ImageUploadEvent.Failed(this@GroupImpl, image, response.resultCode, response.message).broadcast()
|
||||
ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
|
||||
if (response.message == "over file size max") throw OverFileSizeMaxException()
|
||||
error("upload group image failed with reason ${response.message}")
|
||||
}
|
||||
is ImgStore.GroupPicUp.Response.FileExists -> {
|
||||
val resourceId = image.calculateImageResourceId()
|
||||
val resourceId = resource.calculateResourceId()
|
||||
return OfflineGroupImage(imageId = resourceId)
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
is ImgStore.GroupPicUp.Response.RequireUpload -> {
|
||||
HighwayHelper.uploadImageToServers(
|
||||
bot,
|
||||
response.uploadIpList.zip(response.uploadPortList),
|
||||
response.uKey,
|
||||
image.input,
|
||||
resource,
|
||||
kind = "group image",
|
||||
commandId = 2
|
||||
)
|
||||
val resourceId = image.calculateImageResourceId()
|
||||
val resourceId = resource.calculateResourceId()
|
||||
return OfflineGroupImage(imageId = resourceId)
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, image, it).broadcast() }
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
image.input.release()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,7 +283,7 @@ internal class GroupImpl(
|
||||
if (content.size > 1048576) {
|
||||
throw OverFileSizeMaxException()
|
||||
}
|
||||
val md5 = MiraiPlatformUtils.md5(content)
|
||||
val md5 = content.md5()
|
||||
val codec = with(content.copyOfRange(0, 10).toUHexString("")) {
|
||||
when {
|
||||
startsWith("2321414D52") -> 0 // amr
|
||||
|
@ -15,7 +15,7 @@ import net.mamoe.mirai.contact.OtherClientInfo
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class OtherClientImpl(
|
||||
@ -27,7 +27,7 @@ internal class OtherClientImpl(
|
||||
throw UnsupportedOperationException("OtherClientImpl.sendMessage is not yet supported.")
|
||||
}
|
||||
|
||||
override suspend fun uploadImage(image: ExternalImage): Image {
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image {
|
||||
throw UnsupportedOperationException("OtherClientImpl.uploadImage is not yet supported.")
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,8 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.safeCast
|
||||
import net.mamoe.mirai.utils.unzip
|
||||
import net.mamoe.mirai.utils.zip
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
@ -58,7 +60,7 @@ internal fun MessageChain.toRichTextElems(
|
||||
|
||||
fun transformOneMessage(it: Message) {
|
||||
if (it is RichMessage) {
|
||||
val content = MiraiPlatformUtils.zip(it.content.toByteArray())
|
||||
val content = it.content.toByteArray().zip()
|
||||
when (it) {
|
||||
is ForwardMessageInternal -> {
|
||||
elements.add(
|
||||
@ -422,7 +424,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, botId:
|
||||
{ "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) {
|
||||
when (element.lightApp.data[0].toInt()) {
|
||||
0 -> element.lightApp.data.encodeToString(offset = 1)
|
||||
1 -> MiraiPlatformUtils.unzip(element.lightApp.data, 1).encodeToString()
|
||||
1 -> element.lightApp.data.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${element.lightApp.data[0]}")
|
||||
}
|
||||
}
|
||||
@ -432,7 +434,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, botId:
|
||||
val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {
|
||||
when (element.richMsg.template1[0].toInt()) {
|
||||
0 -> element.richMsg.template1.encodeToString(offset = 1)
|
||||
1 -> MiraiPlatformUtils.unzip(element.richMsg.template1, 1).encodeToString()
|
||||
1 -> element.richMsg.template1.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${element.richMsg.template1[0]}")
|
||||
}
|
||||
}
|
||||
|
@ -25,19 +25,31 @@ import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1
|
||||
import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2
|
||||
import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
|
||||
import net.mamoe.mirai.message.data.md5
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.generateImageId
|
||||
|
||||
/*
|
||||
* ImgType:
|
||||
* JPG: 1000
|
||||
* PNG: 1001
|
||||
* WEBP: 1002
|
||||
* BMP: 1005
|
||||
* GIG: 2000 // gig? gif?
|
||||
* APNG: 2001
|
||||
* SHARPP: 1004
|
||||
*/
|
||||
|
||||
internal class OnlineGroupImageImpl(
|
||||
internal val delegate: ImMsgBody.CustomFace
|
||||
) : @Suppress("DEPRECATION")
|
||||
OnlineGroupImage() {
|
||||
override val imageId: String = ExternalImage.generateImageId(
|
||||
override val imageId: String = generateImageId(
|
||||
delegate.md5,
|
||||
delegate.filePath.substringAfterLast('.')
|
||||
).takeIf {
|
||||
GROUP_IMAGE_ID_REGEX.matches(it)
|
||||
} ?: ExternalImage.generateImageId(delegate.md5)
|
||||
} ?: generateImageId(delegate.md5)
|
||||
|
||||
override val originUrl: String
|
||||
get() = if (delegate.origUrl.isBlank()) {
|
||||
@ -145,14 +157,12 @@ internal interface SuspendDeferredOriginUrlAware : Image {
|
||||
@MiraiExperimentalApi("Will be renamed to OfflineImage on 1.2.0")
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
internal class ExperimentalDeferredImage internal constructor(
|
||||
@Suppress("CanBeParameter") private val externalImage: ExternalImage // for future use
|
||||
@Suppress("CanBeParameter") private val externalImage: ExternalResource // for future use
|
||||
) : AbstractImage(), SuspendDeferredOriginUrlAware {
|
||||
override suspend fun getUrl(bot: Bot): String {
|
||||
TODO()
|
||||
}
|
||||
|
||||
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
override val imageId: String = externalImage.calculateImageResourceId()
|
||||
override val imageId: String = externalImage.calculateResourceId()
|
||||
}
|
||||
|
||||
@Suppress("EXPOSED_SUPER_INTERFACE")
|
||||
|
@ -38,7 +38,7 @@ internal val DeviceInfo.guid: ByteArray get() = generateGuid(androidId, macAddre
|
||||
*/
|
||||
@Suppress("RemoveRedundantQualifierName") // bug
|
||||
private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
|
||||
net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(androidId + macAddress)
|
||||
(androidId + macAddress).md5()
|
||||
|
||||
/**
|
||||
* 生成长度为 [length], 元素为随机 `0..255` 的 [ByteArray]
|
||||
@ -317,7 +317,7 @@ internal open class QQAndroidClient(
|
||||
|
||||
@Suppress("RemoveRedundantQualifierName") // bug
|
||||
internal fun generateTgtgtKey(guid: ByteArray): ByteArray =
|
||||
net.mamoe.mirai.internal.utils.MiraiPlatformUtils.md5(getRandomByteArray(16) + guid)
|
||||
(getRandomByteArray(16) + guid).md5()
|
||||
|
||||
|
||||
internal class ReserveUinInfo(
|
||||
|
@ -7,8 +7,6 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.internal.network.highway
|
||||
|
||||
import io.ktor.client.*
|
||||
@ -16,21 +14,25 @@ import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.utils.io.jvm.javaio.*
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.use
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.CSDataHighwayHead
|
||||
import net.mamoe.mirai.internal.utils.*
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.internal.utils.PlatformSocket
|
||||
import net.mamoe.mirai.internal.utils.SocketException
|
||||
import net.mamoe.mirai.internal.utils.addSuppressedMirai
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.internal.utils.io.withUse
|
||||
import net.mamoe.mirai.utils.internal.ReusableInput
|
||||
import net.mamoe.mirai.utils.verbose
|
||||
import net.mamoe.mirai.internal.utils.toIpV4AddressString
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.InputStream
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTime
|
||||
@ -41,7 +43,7 @@ internal suspend fun HttpClient.postImage(
|
||||
htcmd: String,
|
||||
uin: Long,
|
||||
groupcode: Long?,
|
||||
imageInput: ReusableInput,
|
||||
imageInput: ExternalResource,
|
||||
uKeyHex: String
|
||||
): Boolean = post<HttpStatusCode> {
|
||||
url {
|
||||
@ -65,12 +67,10 @@ internal suspend fun HttpClient.postImage(
|
||||
|
||||
body = object : OutgoingContent.WriteChannelContent() {
|
||||
override val contentType: ContentType = ContentType.Image.Any
|
||||
override val contentLength: Long = imageInput.size
|
||||
|
||||
override val contentLength: Long = imageInput.size.toLong()
|
||||
|
||||
override suspend fun writeTo(channel: ByteWriteChannel) {
|
||||
imageInput.writeTo(channel)
|
||||
|
||||
imageInput.inputStream().withUse { copyTo(channel) }
|
||||
}
|
||||
}
|
||||
} == HttpStatusCode.OK
|
||||
@ -82,7 +82,7 @@ internal object HighwayHelper {
|
||||
bot: QQAndroidBot,
|
||||
servers: List<Pair<Int, Int>>,
|
||||
uKey: ByteArray,
|
||||
image: ReusableInput,
|
||||
image: ExternalResource,
|
||||
kind: String,
|
||||
commandId: Int
|
||||
) = uploadImageToServers(bot, servers, uKey, image.md5, image, kind, commandId)
|
||||
@ -94,11 +94,11 @@ internal object HighwayHelper {
|
||||
servers: List<Pair<Int, Int>>,
|
||||
uKey: ByteArray,
|
||||
md5: ByteArray,
|
||||
input: ReusableInput,
|
||||
input: ExternalResource,
|
||||
kind: String,
|
||||
commandId: Int
|
||||
) = servers.retryWithServers(
|
||||
(input.size * 1000 / 1024 / 10).coerceAtLeast(5000),
|
||||
(input.size * 1000 / 1024 / 10).coerceAtLeast(5000).toLong(),
|
||||
onFail = {
|
||||
throw IllegalStateException("cannot upload $kind, failed on all servers.", it)
|
||||
}
|
||||
@ -131,7 +131,7 @@ internal object HighwayHelper {
|
||||
serverIp: String,
|
||||
serverPort: Int,
|
||||
ticket: ByteArray,
|
||||
imageInput: ReusableInput,
|
||||
imageInput: ExternalResource,
|
||||
fileMd5: ByteArray,
|
||||
commandId: Int // group=2, friend=1
|
||||
) {
|
||||
@ -157,8 +157,7 @@ internal object HighwayHelper {
|
||||
ticket = ticket,
|
||||
data = imageInput,
|
||||
fileMd5 = fileMd5
|
||||
).withUse {
|
||||
flow.collect {
|
||||
).useAll {
|
||||
socket.send(it)
|
||||
//0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
|
||||
|
||||
@ -172,7 +171,6 @@ internal object HighwayHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadPttToServers(
|
||||
bot: QQAndroidBot,
|
||||
@ -223,6 +221,87 @@ internal object HighwayHelper {
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChunkedFlowSession<T>(
|
||||
private val input: InputStream,
|
||||
private val buffer: ByteArray,
|
||||
private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
|
||||
) : Closeable {
|
||||
override fun close() {
|
||||
input.close()
|
||||
}
|
||||
|
||||
private var offset = 0L
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
|
||||
runBIO {
|
||||
val size = input.read(buffer)
|
||||
block(mapper(buffer, size, offset))
|
||||
offset += size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun createImageDataPacketSequence(
|
||||
// RequestDataTrans
|
||||
client: QQAndroidClient,
|
||||
command: String,
|
||||
appId: Int,
|
||||
dataFlag: Int = 4096,
|
||||
commandId: Int,
|
||||
localId: Int = 2052,
|
||||
ticket: ByteArray,
|
||||
data: ExternalResource,
|
||||
fileMd5: ByteArray,
|
||||
sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE
|
||||
): ChunkedFlowSession<ByteReadPacket> {
|
||||
ByteArrayPool.checkBufferSize(sizePerPacket)
|
||||
// require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" }
|
||||
|
||||
return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket)) { buffer, size, offset ->
|
||||
val head = CSDataHighwayHead.ReqDataHighwayHead(
|
||||
msgBasehead = CSDataHighwayHead.DataHighwayHead(
|
||||
version = 1,
|
||||
uin = client.uin.toString(),
|
||||
command = command,
|
||||
seq = when (commandId) {
|
||||
2 -> client.nextHighwayDataTransSequenceIdForGroup()
|
||||
1 -> client.nextHighwayDataTransSequenceIdForFriend()
|
||||
27 -> client.nextHighwayDataTransSequenceIdForApplyUp()
|
||||
else -> error("illegal commandId: $commandId")
|
||||
},
|
||||
retryTimes = 0,
|
||||
appid = appId,
|
||||
dataflag = dataFlag,
|
||||
commandId = commandId,
|
||||
localeId = localId
|
||||
),
|
||||
msgSeghead = CSDataHighwayHead.SegHead(
|
||||
// cacheAddr = 812157193,
|
||||
datalength = size,
|
||||
dataoffset = offset,
|
||||
filesize = data.size,
|
||||
serviceticket = ticket,
|
||||
md5 = buffer.md5(0, size),
|
||||
fileMd5 = fileMd5,
|
||||
flag = 0,
|
||||
rtcode = 0
|
||||
),
|
||||
reqExtendinfo = EMPTY_BYTE_ARRAY,
|
||||
msgLoginSigHead = null
|
||||
).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
|
||||
|
||||
buildPacket {
|
||||
writeByte(40)
|
||||
writeInt(head.size)
|
||||
writeInt(size)
|
||||
writeFully(head)
|
||||
writeFully(buffer, 0, size)
|
||||
writeByte(41)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
|
||||
timeoutMillis: Long,
|
||||
@ -249,6 +328,7 @@ internal suspend inline fun List<Pair<Int, Int>>.retryWithServers(
|
||||
onFail(exception)
|
||||
}
|
||||
|
||||
internal fun Int.sizeToString() = this.toLong().sizeToString()
|
||||
internal fun Long.sizeToString(): String {
|
||||
return if (this < 1024) {
|
||||
"$this B"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -25,13 +25,15 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.Heartbeat
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
|
||||
import net.mamoe.mirai.internal.network.readUShortLVByteArray
|
||||
import net.mamoe.mirai.internal.utils.*
|
||||
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
|
||||
import net.mamoe.mirai.internal.utils.io.readPacketExact
|
||||
import net.mamoe.mirai.internal.utils.io.readString
|
||||
import net.mamoe.mirai.internal.utils.io.useBytes
|
||||
import net.mamoe.mirai.internal.utils.io.withUse
|
||||
import net.mamoe.mirai.internal.utils.toInt
|
||||
import net.mamoe.mirai.internal.utils.toReadPacket
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
internal sealed class PacketFactory<TPacket : Packet?> {
|
||||
@ -348,7 +350,7 @@ internal object KnownPacketFactories {
|
||||
1 -> {
|
||||
input.discardExact(4)
|
||||
input.useBytes { data, length ->
|
||||
MiraiPlatformUtils.unzip(data, 0, length).let {
|
||||
data.unzip(0, length).let {
|
||||
val size = it.toInt()
|
||||
if (size == it.size || size == it.size + 4) {
|
||||
it.toReadPacket(offset = 4)
|
||||
|
@ -16,11 +16,11 @@ import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.toByteArray
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.internal.network.protocol.LoginType
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.internal.utils.NetworkType
|
||||
import net.mamoe.mirai.internal.utils.io.*
|
||||
import net.mamoe.mirai.internal.utils.toByteArray
|
||||
import net.mamoe.mirai.utils.currentTimeMillis
|
||||
import net.mamoe.mirai.utils.md5
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
@ -101,8 +101,10 @@ internal fun BytePacketBuilder.t106(
|
||||
guid?.requireSize(16)
|
||||
|
||||
writeShortLVPacket {
|
||||
encryptAndWrite(MiraiPlatformUtils.md5(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt()
|
||||
.toByteArray())) {
|
||||
encryptAndWrite(
|
||||
(passwordMd5 + ByteArray(4) + (salt.takeIf { it != 0L } ?: uin).toInt()
|
||||
.toByteArray()).md5()
|
||||
) {
|
||||
writeShort(4)//TGTGTVer
|
||||
writeInt(Random.nextInt())
|
||||
writeInt(ssoVersion)//ssoVer
|
||||
@ -335,7 +337,7 @@ internal fun BytePacketBuilder.t109(
|
||||
) {
|
||||
writeShort(0x109)
|
||||
writeShortLVPacket {
|
||||
writeFully(MiraiPlatformUtils.md5(androidId))
|
||||
writeFully(androidId.md5())
|
||||
} shouldEqualsTo 16
|
||||
}
|
||||
|
||||
@ -571,7 +573,7 @@ internal fun BytePacketBuilder.t187(
|
||||
) {
|
||||
writeShort(0x187)
|
||||
writeShortLVPacket {
|
||||
writeFully(MiraiPlatformUtils.md5(macAddress)) // may be md5
|
||||
writeFully(macAddress.md5()) // may be md5
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,7 +583,7 @@ internal fun BytePacketBuilder.t188(
|
||||
) {
|
||||
writeShort(0x188)
|
||||
writeShortLVPacket {
|
||||
writeFully(MiraiPlatformUtils.md5(androidId))
|
||||
writeFully(androidId.md5())
|
||||
} shouldEqualsTo 16
|
||||
}
|
||||
|
||||
|
@ -26,17 +26,18 @@ import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.PacketLogger
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.internal.utils._miraiContentToString
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
|
||||
import net.mamoe.mirai.message.data.ForwardMessage
|
||||
import net.mamoe.mirai.message.data.asMessageChain
|
||||
import net.mamoe.mirai.utils.gzip
|
||||
import net.mamoe.mirai.utils.md5
|
||||
|
||||
internal class MessageValidationData(
|
||||
val data: ByteArray,
|
||||
val md5: ByteArray = MiraiPlatformUtils.md5(data)
|
||||
val md5: ByteArray = data.md5()
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "MessageValidationData(data=<size=${data.size}>, md5=${md5.contentToString()})"
|
||||
@ -88,7 +89,7 @@ internal fun Collection<ForwardMessage.INode>.calculateValidationDataForGroup(
|
||||
|
||||
val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer())
|
||||
|
||||
return MessageValidationData(MiraiPlatformUtils.gzip(bytes))
|
||||
return MessageValidationData(bytes.gzip())
|
||||
}
|
||||
|
||||
internal class MultiMsg {
|
||||
|
@ -10,7 +10,6 @@
|
||||
package net.mamoe.mirai.internal.network.protocol.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.use
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import net.mamoe.mirai.event.AbstractEvent
|
||||
@ -23,12 +22,12 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket
|
||||
import net.mamoe.mirai.internal.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.internal.utils.hexToBytes
|
||||
import net.mamoe.mirai.internal.utils.io.ProtoBuf
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.*
|
||||
import net.mamoe.mirai.internal.utils.io.withUse
|
||||
import net.mamoe.mirai.internal.utils.toReadPacket
|
||||
import net.mamoe.mirai.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.utils.verbose
|
||||
import net.mamoe.mirai.internal.network.protocol.data.jce.PushReq as PushReqJceStruct
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
package net.mamoe.mirai.internal.network.protocol.packet.login
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
@ -30,10 +31,13 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.*
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x769
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.*
|
||||
import net.mamoe.mirai.internal.utils.*
|
||||
import net.mamoe.mirai.internal.utils.NetworkType
|
||||
import net.mamoe.mirai.internal.utils._miraiContentToString
|
||||
import net.mamoe.mirai.internal.utils.encodeToString
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.*
|
||||
import net.mamoe.mirai.utils.currentTimeMillis
|
||||
import java.util.concurrent.CancellationException
|
||||
import net.mamoe.mirai.internal.utils.toReadPacket
|
||||
import net.mamoe.mirai.utils.localIpAddress
|
||||
|
||||
@Suppress("EnumEntryName", "unused")
|
||||
internal enum class RegPushReason {
|
||||
@ -157,7 +161,7 @@ internal class StatSvc {
|
||||
strDevType = client.device.model.encodeToString(),
|
||||
strOSVer = client.device.version.release.encodeToString(),
|
||||
uOldSSOIp = 0,
|
||||
uNewSSOIp = MiraiPlatformUtils.localIpAddress().runCatching { ipToLong() }
|
||||
uNewSSOIp = localIpAddress().runCatching { ipToLong() }
|
||||
.getOrElse { "192.168.1.123".ipToLong() },
|
||||
strVendorName = "MIUI",
|
||||
strVendorOSName = "?ONEPLUS A5000_23_17",
|
||||
|
@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.utils.io.*
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.error
|
||||
import net.mamoe.mirai.utils.generateDeviceInfoData
|
||||
import net.mamoe.mirai.utils.md5
|
||||
|
||||
internal class WtLogin {
|
||||
/**
|
||||
@ -80,7 +81,7 @@ internal class WtLogin {
|
||||
t8(2052)
|
||||
t104(client.t104)
|
||||
t116(client.miscBitMap, client.subSigMap)
|
||||
t401(MiraiPlatformUtils.md5(client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402))
|
||||
t401((client.device.guid + "stMNokHgxZUGhsYp".toByteArray() + t402).md5())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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})" }
|
||||
}
|
@ -17,14 +17,11 @@ import kotlinx.io.charsets.Charset
|
||||
import kotlinx.io.charsets.Charsets
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.String
|
||||
import kotlinx.io.core.use
|
||||
import net.mamoe.mirai.internal.utils.io.withUse
|
||||
import net.mamoe.mirai.utils.checkOffsetAndLength
|
||||
import java.util.*
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmOverloads
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
|
||||
@JvmOverloads
|
||||
@ -107,5 +104,5 @@ internal inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
|
||||
contract {
|
||||
callsInPlace(t, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return this.toReadPacket().use(t)
|
||||
return this.toReadPacket().withUse(t)
|
||||
}
|
@ -10,10 +10,9 @@
|
||||
package net.mamoe.mirai.internal.utils.crypto
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.pool.useInstance
|
||||
import net.mamoe.mirai.internal.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.internal.utils.toByteArray
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.utils.ByteArrayPool
|
||||
import kotlin.experimental.and
|
||||
import kotlin.experimental.xor
|
||||
import kotlin.random.Random
|
||||
|
@ -16,14 +16,11 @@ package net.mamoe.mirai.internal.utils.io
|
||||
import kotlinx.io.charsets.Charset
|
||||
import kotlinx.io.charsets.Charsets
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.internal.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.internal.utils.toReadPacket
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.utils.ByteArrayPool
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
internal inline fun <R> ByteReadPacket.useBytes(
|
||||
|
@ -9,6 +9,10 @@
|
||||
package net.mamoe.mirai.internal.utils
|
||||
|
||||
import kotlinx.io.core.toByteArray
|
||||
import net.mamoe.mirai.utils.gzip
|
||||
import net.mamoe.mirai.utils.ungzip
|
||||
import net.mamoe.mirai.utils.unzip
|
||||
import net.mamoe.mirai.utils.zip
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -16,11 +20,11 @@ internal class PlatformUtilsTest {
|
||||
|
||||
@Test
|
||||
fun testZip() {
|
||||
assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString())
|
||||
assertEquals("test", "test".toByteArray().zip().unzip().encodeToString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGZip() {
|
||||
assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString())
|
||||
assertEquals("test", "test".toByteArray().gzip().ungzip().encodeToString())
|
||||
}
|
||||
}
|
@ -14,10 +14,10 @@ package test
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.Input
|
||||
import kotlinx.io.core.readAvailable
|
||||
import kotlinx.io.pool.useInstance
|
||||
import net.mamoe.mirai.internal.utils.ByteArrayPool
|
||||
import kotlinx.io.core.use
|
||||
import net.mamoe.mirai.internal.utils.toReadPacket
|
||||
import net.mamoe.mirai.internal.utils.toUHexString
|
||||
import net.mamoe.mirai.utils.ByteArrayPool
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.MiraiLoggerWithSwitch
|
||||
import net.mamoe.mirai.utils.withSwitch
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
package net.mamoe.mirai.internal.utils.crypto
|
||||
|
||||
import net.mamoe.mirai.internal.utils.MiraiPlatformUtils
|
||||
import net.mamoe.mirai.utils.md5
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.*
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
@ -81,7 +81,7 @@ internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
|
||||
val instance = KeyAgreement.getInstance("ECDH", "BC")
|
||||
instance.init(privateKey)
|
||||
instance.doPhase(publicKey, true)
|
||||
return MiraiPlatformUtils.md5(instance.generateSecret())
|
||||
return instance.generateSecret().md5()
|
||||
}
|
||||
|
||||
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
|
||||
|
Loading…
Reference in New Issue
Block a user