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