1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-24 20:43:33 +08:00

[core] initial support for decoding FriendFileMessage, sending friend file

This commit is contained in:
StageGuard 2023-09-01 10:40:46 +08:00
parent ab5d08afb1
commit ecee497ec6
No known key found for this signature in database
GPG Key ID: F6FF8760A883492B
27 changed files with 771 additions and 101 deletions

View File

@ -10,12 +10,17 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.ProgressionCallback
/**
* 支持文件操作的 [Contact]. 目前仅 [Group].
* 支持文件操作的 [Contact]. 目前仅 [Group] [Friend].
*
* 获取文件操作相关示例: [RemoteFiles]
*
@ -47,4 +52,18 @@ public interface FileSupported : Contact {
* @since 2.8
*/
public val files: RemoteFiles
/**
* 上传一个文件到联系人并返回文件消息.
* 对于群, 上传到群文件根目录.
*
* @param filename 文件名, 不可包含路径符(slash "/")
* @see AbsoluteFolder.uploadNewFile
* @since 2.17
*/
public suspend fun uploadFile(
filename: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>? = null
): FileMessage
}

View File

@ -37,7 +37,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
*/
@Suppress("RedundantSetter")
@NotStableForInheritance
public interface Friend : User, CoroutineScope, AudioSupported, RoamingSupported {
public interface Friend : User, CoroutineScope, AudioSupported, RoamingSupported, FileSupported {
/**
* 该好友所在的好友分组

View File

@ -53,6 +53,8 @@ public interface AbsoluteFile : AbsoluteFileFolder {
*
* 注意该操作有可能产生同名文件或目录 ( [folder] 中已经存在一个名称为 [name] 的文件或目录时).
*
* 对于好友文件, 此方法不会产生任何效果.
*
* @throws IOException 当发生网络错误时可能抛出
* @throws IllegalStateException 当发生已知的协议错误时抛出
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)

View File

@ -137,7 +137,7 @@ private object MiraiCodeParsers : AbstractMap<String, MiraiCodeParser>(), Map<St
MusicShare(MusicKind.valueOf(kind), title, summary, jumpUrl, pictureUrl, musicUrl, brief)
},
"file" to MiraiCodeParser(Regex("""(.*?),(.*?),(.*?),(.*?)""")) { (id, internalId, name, size) ->
FileMessage(id, internalId.toInt(), name, size.toLong())
FileMessage(id, internalId.toInt(), name, size.toLong()) // TODO: parser for friend file
},
)

View File

@ -7,7 +7,12 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
@file:Suppress(
"INVISIBLE_MEMBER",
"INVISIBLE_REFERENCE",
"CANNOT_OVERRIDE_INVISIBLE_MEMBER",
"DEPRECATION_ERROR"
)
package net.mamoe.mirai.mock.internal.contact
@ -15,15 +20,14 @@ import kotlinx.coroutines.cancel
import net.mamoe.mirai.contact.AvatarSpec
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.OtherClient
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.OfflineAudio
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.mock.MockBot
import net.mamoe.mirai.mock.contact.MockFriend
import net.mamoe.mirai.mock.internal.contact.friendfroup.MockFriendGroups
@ -34,6 +38,8 @@ import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToFriend
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
import net.mamoe.mirai.mock.utils.broadcastBlocking
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.ProgressionCallback
import net.mamoe.mirai.utils.RemoteFile
import net.mamoe.mirai.utils.cast
import java.util.concurrent.CancellationException
import kotlin.coroutines.CoroutineContext
@ -92,6 +98,21 @@ internal class MockFriendImpl(
FriendRemarkChangeEvent(this, ov, value).broadcastBlocking()
}
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.ERROR)
override val filesRoot: RemoteFile
get() = throw UnsupportedOperationException("file system is not supported by MockFriend, please use uploadFile instead.")
override val files: RemoteFiles
get() = throw UnsupportedOperationException("file system is not supported by MockFriend, please use uploadFile instead.")
override suspend fun uploadFile(
filename: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?
): FileMessage {
TODO("Not yet implemented")
}
override fun newMessagePreSend(message: Message): MessagePreSendEvent {
return FriendMessagePreSendEvent(this, message)
}

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.announcement.OfflineAnnouncement
import net.mamoe.mirai.contact.announcement.buildAnnouncementParameters
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.data.GroupHonorType
@ -350,6 +351,14 @@ internal class MockGroupImpl(
net.mamoe.mirai.mock.internal.remotefile.absolutefile.MockRemoteFiles(this, txFileSystem)
}
override suspend fun uploadFile(
filename: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?
): FileMessage {
TODO("uploadFile of MockGroupImpl")
}
override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio =
resource.mockUploadAudio(bot)

View File

@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.firstOrNull
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.GroupFileMessageImpl
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.mock.internal.remotefile.remotefile.MockRemoteFile
import net.mamoe.mirai.mock.resserver.MockServerRemoteFile
@ -55,7 +55,7 @@ internal class MockAbsoluteFile(
override fun toMessage(): FileMessage {
//todo busId
return FileMessageImpl(id, 0, name, size)
return GroupFileMessageImpl(id, 0, name, size)
}
override suspend fun refreshed(): AbsoluteFile? =

View File

@ -17,7 +17,7 @@ import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.GroupFileMessageImpl
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.mock.contact.MockGroup
@ -284,7 +284,7 @@ internal class MockRemoteFile(
override suspend fun toMessage(): FileMessage? {
val resolved = resolveFile() ?: return null
return FileMessageImpl(
return GroupFileMessageImpl(
name = resolved.name,
id = resolved.id,
size = resolved.size,
@ -308,7 +308,7 @@ internal class MockRemoteFile(
val rsp = parent.uploadFile(this.name, resource, contact.bot.id)
callback?.onProgression(this, resource, rsSize)
callback?.onSuccess(this, resource)
return FileMessageImpl(
return GroupFileMessageImpl(
name = rsp.name,
id = rsp.id,
size = rsp.size,

View File

@ -621,7 +621,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
}
override fun createFileMessage(id: String, internalId: Int, name: String, size: Long): FileMessage {
return FileMessageImpl(id, internalId, name, size)
return GroupFileMessageImpl(id, internalId, name, size)
}
override fun createUnsupportedMessage(struct: ByteArray): UnsupportedMessage =

View File

@ -18,6 +18,8 @@ import io.ktor.utils.io.core.*
import kotlinx.coroutines.launch
import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.event.broadcast
@ -25,15 +27,25 @@ import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
import net.mamoe.mirai.event.events.FriendRemarkChangeEvent
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.file.AbsoluteFriendFileImpl
import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
import net.mamoe.mirai.internal.contact.roaming.RoamingMessagesImplFriend
import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
import net.mamoe.mirai.internal.message.flags.AllowSendFileMessage
import net.mamoe.mirai.internal.message.protocol.outgoing.FriendMessageProtocolStrategy
import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy
import net.mamoe.mirai.internal.network.components.HttpClientProvider
import net.mamoe.mirai.internal.network.highway.*
import net.mamoe.mirai.internal.network.protocol
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346
import net.mamoe.mirai.internal.network.protocol.data.proto.ExcitingBusiInfo
import net.mamoe.mirai.internal.network.protocol.data.proto.ExcitingClientInfo
import net.mamoe.mirai.internal.network.protocol.data.proto.ExcitingFileEntry
import net.mamoe.mirai.internal.network.protocol.data.proto.ExcitingFileNameInfo
import net.mamoe.mirai.internal.network.protocol.data.proto.FileUploadEntry
import net.mamoe.mirai.internal.network.protocol.data.proto.FileUploadExt
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.packet.chat.OfflineFilleHandleSvr
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
@ -41,6 +53,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.summarycard.ChangeFriend
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.OfflineAudio
import net.mamoe.mirai.spi.AudioToSilkService
@ -89,6 +102,102 @@ internal class FriendImpl(
private val messageProtocolStrategy: MessageProtocolStrategy<FriendImpl> = FriendMessageProtocolStrategy(this)
override val files: RemoteFiles
get() = throw UnsupportedOperationException("file system is not supported by Friend, please use uploadFile instead.")
@Suppress("DEPRECATION_ERROR")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.ERROR)
override val filesRoot: RemoteFile
get() = throw UnsupportedOperationException("file system is not supported by Friend, please use uploadFile instead.")
override suspend fun uploadFile(
filename: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?
): FileMessage {
val md5 = content.md5
val sha1 = content.sha1
val size = content.size
val appUpResp = bot.network.sendAndExpect(
OfflineFilleHandleSvr.ApplyUploadV3(bot.client, this, filename, size, md5, sha1)
)
if (appUpResp is OfflineFilleHandleSvr.ApplyUploadV3.Response.Failed) {
throw IllegalStateException(appUpResp.message)
}
val fileUuid = when (appUpResp) {
is OfflineFilleHandleSvr.ApplyUploadV3.Response.FileExists -> appUpResp.fileUuid
is OfflineFilleHandleSvr.ApplyUploadV3.Response.RequireUpload -> appUpResp.fileUuid
else -> error("unreachable!")
}
val file = AbsoluteFriendFileImpl(
this,
fileUuid.decodeToString(),
filename,
bot.id,
0,
content.size,
content.sha1,
content.md5
)
if (appUpResp is OfflineFilleHandleSvr.ApplyUploadV3.Response.RequireUpload) {
val ext = FileUploadExt(
u1 = 100,
u2 = 2,
entry = FileUploadEntry(
business = ExcitingBusiInfo(
busId = 3,
senderUin = bot.uin,
receiverUin = uin,
groupCode = 0,
),
fileEntry = ExcitingFileEntry(
fileSize = content.size,
md5 = content.md5,
sha1 = content.sha1,
fileId = appUpResp.fileUuid,
uploadKey = appUpResp.uploadKey,
),
clientInfo = ExcitingClientInfo(
clientType = 2,
appId = bot.client.protocol.id.toString(),
terminalType = 2,
clientVer = "d92615c5",
unknown = 4,
),
fileNameInfo = ExcitingFileNameInfo(filename)
),
u3 = 0,
u200 = null
)
Highway.uploadResourceBdh(
bot = bot,
resource = content,
kind = ResourceKind.FRIEND_FILE,
commandId = 69,
extendInfo = ext.toByteArray(FileUploadExt.serializer()),
dataFlag = 0,
callback = if (callback == null) null else fun(it: Long) {
callback.onProgression(file, content, it)
}
)
callback?.onFinished(file, content, Result.success(content.size))
}
val upSuccResp = bot.network.sendAndExpect(OfflineFilleHandleSvr.UploadSucc(bot.client, this, fileUuid))
if (upSuccResp is OfflineFilleHandleSvr.FileInfo.Failed) {
throw IllegalStateException(upSuccResp.message)
}
sendMessage(AllowSendFileMessage + file.toMessage())
return file.toMessage()
}
override suspend fun delete() {
check(bot.friends[id] != null) {
"Friend $id had already been deleted"

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.active.GroupActive
import net.mamoe.mirai.contact.announcement.Announcements
import net.mamoe.mirai.contact.essence.Essences
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.data.GroupHonorType
@ -395,6 +396,16 @@ internal abstract class CommonGroupImpl constructor(
override val roamingMessages: RoamingMessages by lazy { RoamingMessagesImplGroup(this) }
override suspend fun uploadFile(
filename: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?,
): FileMessage {
val tailFileName = filename.split('/').last().trim()
val absFile = files.uploadNewFile("/$tailFileName", content, callback)
return absFile.toMessage()
}
// 鉴于在 [essences] 中 有相同的功能的 Web API 所以此方法移除
// override suspend fun removeEssenceMessage(source: MessageSource): Boolean {
// checkBotPermission(MemberPermission.ADMINISTRATOR)

View File

@ -59,7 +59,7 @@ internal fun CommonAbsoluteFolderImpl.createChildFolder(
internal fun CommonAbsoluteFolderImpl.createChildFile(
info: GroupFileCommon.FileInfo
): AbsoluteFileImpl = AbsoluteFileImpl(
): AbsoluteGroupFileImpl = AbsoluteGroupFileImpl(
contact = contact,
parent = this,
id = info.fileId,
@ -74,12 +74,18 @@ internal fun CommonAbsoluteFolderImpl.createChildFile(
busId = info.busId
)
/**
* only for group
*/
internal expect class AbsoluteFolderImpl(
contact: FileSupported, parent: AbsoluteFolder?, id: String, name: String,
uploadTime: Long, uploaderId: Long, lastModifiedTime: Long,
contentsCount: Int,
) : CommonAbsoluteFolderImpl
/**
* only for group
*/
internal abstract class CommonAbsoluteFolderImpl(
contact: FileSupported, parent: AbsoluteFolder?, id: String, name: String,
uploadTime: Long, uploaderId: Long, lastModifiedTime: Long,
@ -160,7 +166,7 @@ internal abstract class CommonAbsoluteFolderImpl(
-36 -> folder.throwPermissionDeniedException("uploadNewFile")
}
val file = AbsoluteFileImpl(
val file = AbsoluteGroupFileImpl(
contact = folder.contact,
parent = folder,
id = resp.fileId,
@ -187,10 +193,10 @@ internal abstract class CommonAbsoluteFolderImpl(
return file
}
val ext = GroupFileUploadExt(
val ext = FileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
entry = FileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = folder.bot.id,
@ -227,7 +233,8 @@ internal abstract class CommonAbsoluteFolderImpl(
),
),
u3 = 0,
).toByteArray(GroupFileUploadExt.serializer())
u200 = 1
).toByteArray(FileUploadExt.serializer())
callback?.onBegin(file, content)

View File

@ -0,0 +1,115 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.file
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.message.data.FriendFileMessageImpl
import net.mamoe.mirai.internal.network.protocol.packet.chat.OfflineFilleHandleSvr
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.currentTimeSeconds
import net.mamoe.mirai.utils.warning
internal class AbsoluteFriendFileImpl(
override var contact: FileSupported,
override var id: String,
override var name: String,
override var uploaderId: Long,
override var expiryTime: Long,
override val size: Long,
override val sha1: ByteArray,
override val md5: ByteArray,
) : AbsoluteFile {
private inline val bot get() = contact.bot.asQQAndroidBot()
private inline val client get() = bot.client
override var parent: AbsoluteFolder? = null
override val absolutePath: String
get() = "/"
override val isFile: Boolean
get() = true
override val isFolder: Boolean
get() = false
override var lastModifiedTime: Long = 0
override suspend fun moveTo(folder: AbsoluteFolder): Boolean {
throw UnsupportedOperationException("AbsoluteFile.moveTo is not implemented in FriendFile.")
}
override suspend fun getUrl(): String? {
val resp = bot.network.sendAndExpect(OfflineFilleHandleSvr.ApplyDownload(client, id.encodeToByteArray()))
return if (resp is OfflineFilleHandleSvr.ApplyDownload.Response.Success) {
resp.url
} else {
null
}
}
override fun toMessage(): FileMessage {
return FriendFileMessageImpl(id, name, size, false)
}
override suspend fun refreshed(): AbsoluteFile? {
val queryResp = bot.network.sendAndExpect(OfflineFilleHandleSvr.FileQuery(client, id.encodeToByteArray()))
if (queryResp is OfflineFilleHandleSvr.FileInfo.Failed) {
contact.bot.logger.warning { "failed to query friend file info: ${queryResp.message}" }
return null
}
val fileInfo = queryResp as OfflineFilleHandleSvr.FileInfo.Success
return AbsoluteFriendFileImpl(
contact,
fileInfo.fileUuid.decodeToString(),
fileInfo.filename,
fileInfo.ownerUin,
fileInfo.expiryTime,
fileInfo.fileSize,
fileInfo.fileMd5,
fileInfo.fileSha1,
)
}
override suspend fun exists(): Boolean {
val queryResp = bot.network.sendAndExpect(OfflineFilleHandleSvr.FileQuery(client, id.encodeToByteArray()))
if (queryResp is OfflineFilleHandleSvr.FileInfo.Failed) {
contact.bot.logger.warning { "failed to query friend file info: ${queryResp.message}" }
return false
}
val fileInfo = queryResp as OfflineFilleHandleSvr.FileInfo.Success
return fileInfo.expiryTime >= currentTimeSeconds()
}
override suspend fun renameTo(newName: String): Boolean {
throw UnsupportedOperationException("AbsoluteFile.renameTo is not implemented in FriendFile.")
}
override suspend fun delete(): Boolean {
throw UnsupportedOperationException("AbsoluteFile.delete is not implemented in FriendFile.")
}
override suspend fun refresh(): Boolean {
return refreshed() == null
}
override fun toString(): String {
return "AbsoluteFriendFile(name=$name, id=$id)"
}
override var uploadTime: Long = 0
}

View File

@ -13,14 +13,14 @@ import io.ktor.utils.io.core.*
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.contact.file.AbsoluteFolder
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.GroupFileMessageImpl
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.isSameClass
import net.mamoe.mirai.utils.toUHexString
internal class AbsoluteFileImpl(
internal class AbsoluteGroupFileImpl(
contact: FileSupported,
parent: AbsoluteFolder?,
id: String,
@ -131,7 +131,7 @@ internal class AbsoluteFileImpl(
}
override fun toMessage(): FileMessage {
return FileMessageImpl(id, busId, name, size)
return GroupFileMessageImpl(id, busId, name, size)
}
override suspend fun refresh(): Boolean {
@ -143,7 +143,7 @@ internal class AbsoluteFileImpl(
return true
}
override fun toString(): String = "AbsoluteFile(name=$name, absolutePath=$absolutePath, id=$id)"
override fun toString(): String = "AbsoluteGroupFile(name=$name, absolutePath=$absolutePath, id=$id)"
override suspend fun refreshed(): AbsoluteFile? {
val result = bot.network.sendAndExpect(FileManagement.GetFileInfo(client, contact.id, id, busId))
@ -160,7 +160,7 @@ internal class AbsoluteFileImpl(
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AbsoluteFileImpl || !isSameClass(this, other)) return false
if (other !is AbsoluteGroupFileImpl || !isSameClass(this, other)) return false
if (!super.equals(other)) return false
if (expiryTime != other.expiryTime) return false

View File

@ -25,7 +25,7 @@ import net.mamoe.mirai.utils.isSameType
internal fun AbstractAbsoluteFileFolder.api(): AbsoluteFileFolder = this.cast()
internal fun AbsoluteFileFolder.impl(): AbstractAbsoluteFileFolder = this.cast()
internal fun AbsoluteFile.impl(): AbsoluteFileImpl = this.cast()
internal fun AbsoluteFile.impl(): AbsoluteGroupFileImpl = this.cast()
internal fun AbsoluteFolder.impl(): AbsoluteFolderImpl = this.cast()
internal val AbsoluteFolder?.idOrRoot get() = this?.id ?: AbsoluteFolder.ROOT_FOLDER_ID

View File

@ -14,9 +14,7 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio
import net.mamoe.mirai.internal.message.data.LongMessageInternal
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade
import net.mamoe.mirai.internal.message.protocol.impl.PokeMessageProtocol.Companion.UNSUPPORTED_POKE_MESSAGE_PLAIN
import net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol.Companion.UNSUPPORTED_MERGED_MESSAGE_PLAIN
@ -24,9 +22,7 @@ import net.mamoe.mirai.internal.message.source.*
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.structureToString
import net.mamoe.mirai.utils.toLongUnsigned
import net.mamoe.mirai.utils.warning
/**
@ -138,7 +134,6 @@ private fun List<MsgComm.Msg>.toMessageChainImpl(
): MessageChain {
val messageList = this
val builder = MessageChainBuilder(messageList.sumOf { it.msgBody.richText.elems.size })
val source = if (onlineSource != null) {
@ -165,10 +160,6 @@ private fun List<MsgComm.Msg>.toMessageChainImpl(
)
}
for (msg in messageList) {
msg.msgBody.richText.ptt?.toAudio()?.let { builder.add(it) }
}
return builder.build().cleanupRubbishMessageElements()
}
@ -301,14 +292,4 @@ internal object ReceiveMessageTransformer {
return builder.asMessageChain()
}
fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl(
filename = fileName.decodeToString(),
fileMd5 = fileMd5,
fileSize = fileSize.toLongUnsigned(),
codec = AudioCodec.fromId(format),
url = downPara.decodeToString(),
length = time.toLongUnsigned(),
originalPtt = this,
)
}

View File

@ -24,20 +24,22 @@ import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.file.*
import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x6d8.GetFileListRspBody
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.OfflineFilleHandleSvr
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.warning
import kotlin.contracts.contract
internal fun FileMessage.checkIsImpl(): FileMessageImpl {
contract { returns() implies (this@checkIsImpl is FileMessageImpl) }
return this as? FileMessageImpl ?: error("FileMessage must not be implemented manually.")
internal fun FileMessage.checkIsImpl(): GroupFileMessageImpl {
contract { returns() implies (this@checkIsImpl is GroupFileMessageImpl) }
return this as? GroupFileMessageImpl ?: error("FileMessage must not be implemented manually.")
}
@Serializable
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST") // bug
@SerialName(FileMessage.SERIAL_NAME)
internal data class FileMessageImpl(
internal data class GroupFileMessageImpl(
override val id: String,
@SerialName("internalId") val busId: Int,
override val name: String,
@ -84,4 +86,41 @@ internal data class FileMessageImpl(
}
override fun toString(): String = "[mirai:file:$name, id=$id, internalId=$busId, size=$size]"
}
@SerialName(FileMessage.SERIAL_NAME)
internal data class FriendFileMessageImpl(
override val id: String,
override val name: String,
override val size: Long,
@Transient val allowSend: Boolean = false,
) : FileMessage {
override val internalId: Int
get() = 0
override suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? {
val queryResp = contact.bot.asQQAndroidBot().network
.sendAndExpect(
OfflineFilleHandleSvr.FileQuery(contact.bot.asQQAndroidBot().client, id.encodeToByteArray())
)
if (queryResp is OfflineFilleHandleSvr.FileInfo.Failed) {
contact.bot.logger.warning { "failed to query friend file info: ${queryResp.message}" }
return null
}
val fileInfo = queryResp as OfflineFilleHandleSvr.FileInfo.Success
return AbsoluteFriendFileImpl(
contact,
fileInfo.fileUuid.decodeToString(),
fileInfo.filename,
fileInfo.ownerUin,
fileInfo.expiryTime,
fileInfo.fileSize,
fileInfo.fileMd5,
fileInfo.fileSha1,
)
}
override fun toString(): String = "[mirai:file:$name, id=$id, size=$size]"
}

View File

@ -13,11 +13,17 @@ import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
import net.mamoe.mirai.internal.message.protocol.MessageProtocol
import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder
import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoderContext
import net.mamoe.mirai.internal.message.protocol.serialization.MessageSerializer
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.toLongUnsigned
internal class AudioProtocol : MessageProtocol() {
override fun ProcessorCollector.collectProcessorsImpl() {
add(Decoder())
MessageSerializer.superclassesScope(
OnlineAudio::class,
Audio::class,
@ -44,4 +50,22 @@ internal class AudioProtocol : MessageProtocol() {
)
}
}
private class Decoder : MessageDecoder {
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
val originalMsg = runCatching { attributes[MessageDecoderContext.CONTAINING_MSG] }
.getOrNull() ?: return
val ptt = originalMsg.msgBody.richText.ptt ?: return
collect(OnlineAudioImpl(
filename = ptt.fileName.decodeToString(),
fileMd5 = ptt.fileMd5,
fileSize = ptt.fileSize.toLongUnsigned(),
codec = AudioCodec.fromId(ptt.format),
url = ptt.downPara.decodeToString(),
length = ptt.time.toLongUnsigned(),
originalPtt = ptt,
))
}
}
}

View File

@ -13,7 +13,8 @@ import io.ktor.utils.io.core.*
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import net.mamoe.mirai.internal.contact.SendMessageStep
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.FriendFileMessageImpl
import net.mamoe.mirai.internal.message.data.GroupFileMessageImpl
import net.mamoe.mirai.internal.message.data.checkIsImpl
import net.mamoe.mirai.internal.message.flags.AllowSendFileMessage
import net.mamoe.mirai.internal.message.protocol.MessageProtocol
@ -32,7 +33,9 @@ import net.mamoe.mirai.internal.message.source.createMessageReceipt
import net.mamoe.mirai.internal.message.visitor.MessageVisitorEx
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.ObjMsg
import net.mamoe.mirai.internal.network.protocol.data.proto.SubMsgType0x4
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.MessageChain
@ -60,8 +63,8 @@ internal class FileMessageProtocol : MessageProtocol() {
MessageSerializer.superclassesScope(FileMessage::class, MessageContent::class, SingleMessage::class) {
add(
MessageSerializer(
FileMessageImpl::class,
FileMessageImpl.serializer()
GroupFileMessageImpl::class,
GroupFileMessageImpl.serializer()
)
)
}
@ -85,7 +88,7 @@ internal class FileMessageProtocol : MessageProtocol() {
override fun visitFileMessage(message: FileMessage, data: Unit) {
if (ALLOW_SENDING_FILE_MESSAGE) return
// #1715
if (message !is FileMessageImpl) error("Customized FileMessage cannot be send")
if (message !is GroupFileMessageImpl) error("Customized FileMessage cannot be send")
if (!message.allowSend) {
hasFileMessage = true
}
@ -110,7 +113,7 @@ internal class FileMessageProtocol : MessageProtocol() {
val file = currentMessageChain[FileMessage] ?: return
markAsConsumed()
file.checkIsImpl()
file.checkIsImpl() // TODO: file check impl
val contact = attributes[CONTACT]
val bot = contact.bot
@ -133,43 +136,58 @@ internal class FileMessageProtocol : MessageProtocol() {
private class Decoder : MessageDecoder {
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
if (data.transElemInfo == null) return
if (data.transElemInfo.elemType != 24) return
if (data.transElemInfo != null && data.transElemInfo.elemType == 24) {
markAsConsumed()
data.transElemInfo.elemValue.read {
// group file feed
// 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63
// fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? {
// if (file.ext.isEmpty()) return null
// val element = kotlin.runCatching {
// jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject
// }.getOrNull() ?: return null
// val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null
// return extInfo.loadAs(HummerResv21.ResvAttr.serializer())
// }
markAsConsumed()
val var7 = readByte()
if (var7 == 1.toByte()) {
while (remaining > 2) {
val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readShort().toUShort().toInt())
// proto.msgType=6
data.transElemInfo.elemValue.read {
// group file feed
// 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63
// fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? {
// if (file.ext.isEmpty()) return null
// val element = kotlin.runCatching {
// jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject
// }.getOrNull() ?: return null
// val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null
// return extInfo.loadAs(HummerResv21.ResvAttr.serializer())
// }
val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only.
// val attr = getFileRsrvAttr(file) ?: continue
// val info = attr.forwardExtFileInfo ?: continue
val var7 = readByte()
if (var7 == 1.toByte()) {
while (remaining > 2) {
val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readShort().toUShort().toInt())
// proto.msgType=6
val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only.
// val attr = getFileRsrvAttr(file) ?: continue
// val info = attr.forwardExtFileInfo ?: continue
collect(
FileMessageImpl(
id = file.filePath,
busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a
name = file.fileName,
size = file.fileSize
collect(
GroupFileMessageImpl(
id = file.filePath,
busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a
name = file.fileName,
size = file.fileSize
)
)
)
}
}
}
return
}
val originalMsg = runCatching { attributes[MessageDecoderContext.CONTAINING_MSG] }.getOrNull()
if (originalMsg != null && originalMsg.msgHead.msgType == 529) {
markAsConsumed()
val sub0x4 = originalMsg.msgBody.msgContent.loadAs(SubMsgType0x4.MsgBody.serializer())
if (sub0x4.msgNotOnlineFile != null) {
collect(
FriendFileMessageImpl(
sub0x4.msgNotOnlineFile.fileUuid.decodeToString(),
sub0x4.msgNotOnlineFile.fileName.decodeToString(),
sub0x4.msgNotOnlineFile.fileSize
)
)
}
return
}
}
}

View File

@ -121,6 +121,7 @@ internal enum class ResourceKind(
GROUP_AUDIO("group audio"),
GROUP_FILE("group file"),
FRIEND_FILE("friend file"),
LONG_MESSAGE("long message"),
FORWARD_MESSAGE("forward message"),

View File

@ -15,19 +15,28 @@ import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.contact.*
import net.mamoe.mirai.internal.getGroupByUinOrCode
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer
import net.mamoe.mirai.internal.message.RefineContextKey
import net.mamoe.mirai.internal.message.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.FriendFileMessageImpl
import net.mamoe.mirai.internal.message.toMessageChainOnline
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.components.NoticePipelineContext
import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC
import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.fromSync
import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor
import net.mamoe.mirai.internal.network.components.SsoProcessor
import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.internal.network.protocol.data.proto.SubMsgType0x4
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSourceKind
import net.mamoe.mirai.message.data.buildMessageChain
import net.mamoe.mirai.message.data.toMessageChain
import net.mamoe.mirai.utils.assertUnreachable
import net.mamoe.mirai.utils.context
@ -90,7 +99,8 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor<MsgComm.Msg>(type
if (!bot.components[SsoProcessor].firstLoginSucceed) return
val senderUin = if (fromSync) msgHead.toUin else msgHead.fromUin
when (msgHead.msgType) {
166, 167, // 单向好友
166,
167, // 单向好友
208, // friend ptt, maybe also support stranger
-> {
data.msgBody.richText.ptt?.let { ptt ->
@ -118,6 +128,13 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor<MsgComm.Msg>(type
handlePrivateMessage(data, group[senderUin] ?: return)
}
529, // friend file
-> {
val content = msgBody.msgContent
if (content.isEmpty()) return
handlePrivateMessage(data, bot.getFriend(senderUin)?.impl() ?: return)
}
else -> markNotConsumed()
}
@ -143,11 +160,20 @@ internal class PrivateMessageProcessor : SimpleNoticeProcessor<MsgComm.Msg>(type
RefineContextKey.GroupIdOrZero to 0L,
)
)
val time = msgHead.msgTime
collected += if (fromSync) {
val client = bot.otherClients.find { it.appId == msgHead.fromInstid }
?: return // don't compare with dstAppId. diff.
collected += constructMessageEvent(fromSync, msgHead.fromInstid, user, chain, msgHead.msgTime)
}
private fun NoticePipelineContext.constructMessageEvent(
fromSync: Boolean,
fromInstid: Int,
user: AbstractUser,
chain: MessageChain,
time: Int
): MessageEvent? {
return if (fromSync) {
val client = bot.otherClients.find { it.appId == fromInstid }
?: return null // don't compare with dstAppId. diff.
when (user) {
is FriendImpl -> FriendMessageSyncEvent(client, user, chain, time)
is StrangerImpl -> StrangerMessageSyncEvent(client, user, chain, time)

View File

@ -18,6 +18,15 @@ import kotlin.jvm.JvmField
@Serializable
internal class Cmd0x346 : ProtoBuf {
@Serializable
internal class Addr(
@JvmField @ProtoNumber(1) val outIp: Int = 0,
@JvmField @ProtoNumber(2) val outPort: Int = 0,
@JvmField @ProtoNumber(3) val innerIp: Int = 0,
@JvmField @ProtoNumber(4) val innerPort: Int = 0,
@JvmField @ProtoNumber(5) val ipType: Int = 0,
) : ProtoBuf
@Serializable
internal class AddrList(
@JvmField @ProtoNumber(2) val strIp: List<String> = emptyList(),
@ -80,6 +89,7 @@ internal class Cmd0x346 : ProtoBuf {
internal class ApplyDownloadAbsReq(
@JvmField @ProtoNumber(10) val uin: Long = 0L,
@JvmField @ProtoNumber(20) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(30) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -94,8 +104,11 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(10) val uin: Long = 0L,
@JvmField @ProtoNumber(20) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(30) val ownerType: Int = 0,
@JvmField @ProtoNumber(50) val filetype: Int = 0,
@JvmField @ProtoNumber(60) val fileidcrc: String = "",
@JvmField @ProtoNumber(500) val extUintype: Int = 0,
@JvmField @ProtoNumber(501) val needHttpsUrl: Int = 0,
@JvmField @ProtoNumber(600) val fileid: String = "",
) : ProtoBuf
@Serializable
@ -104,6 +117,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(20) val retMsg: String = "",
@JvmField @ProtoNumber(30) val msgDownloadInfo: Cmd0x346.DownloadInfo? = null,
@JvmField @ProtoNumber(40) val msgFileInfo: Cmd0x346.FileInfo? = null,
@JvmField @ProtoNumber(50) val fileSha: ByteArray = EMPTY_BYTE_ARRAY,
) : ProtoBuf
@Serializable
@ -113,6 +127,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(30) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(40) val dangerLevel: Int = 0,
@JvmField @ProtoNumber(50) val totalSpace: Long = 0L,
@JvmField @ProtoNumber(60) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -122,6 +137,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(30) val totalSpace: Long = 0L,
@JvmField @ProtoNumber(40) val usedSpace: Long = 0L,
@JvmField @ProtoNumber(50) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(60) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -275,6 +291,9 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(70) val localFilepath: String = "",
@JvmField @ProtoNumber(80) val dangerLevel: Int = 0,
@JvmField @ProtoNumber(90) val totalSpace: Long = 0L,
@JvmField @ProtoNumber(100) val contenttype: Int /* enum */ = 0,
@JvmField @ProtoNumber(110) val md5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(120) val _3sha: ByteArray = EMPTY_BYTE_ARRAY,
) : ProtoBuf
@Serializable
@ -337,6 +356,9 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(150) val uploadHttpsDomain: String = "",
@JvmField @ProtoNumber(160) val uploadDns: String = "",
@JvmField @ProtoNumber(170) val uploadLanip: String = "",
@JvmField @ProtoNumber(200) val fileidcrc: String = "",
@JvmField @ProtoNumber(210) val rtpMediaPlatformUploadAddress: List<Cmd0x346.Addr> = emptyList(),
@JvmField @ProtoNumber(220) val mediaPlateformUploadKey: ByteArray = EMPTY_BYTE_ARRAY,
) : ProtoBuf
@Serializable
@ -345,6 +367,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(20) val peerUin: Long = 0L,
@JvmField @ProtoNumber(30) val deleteType: Int = 0,
@JvmField @ProtoNumber(40) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(50) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -374,12 +397,15 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(80) val httpsPort: Int = 443,
@JvmField @ProtoNumber(90) val httpsDownloadDomain: String = "",
@JvmField @ProtoNumber(110) val downloadDns: String = "",
@JvmField @ProtoNumber(120) val mediaPlatformDownloadKey: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(130) val downloadipv6List: List<String> = emptyList(),
) : ProtoBuf
@Serializable
internal class DownloadSuccReq(
@JvmField @ProtoNumber(10) val uin: Long = 0L,
@JvmField @ProtoNumber(20) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(30) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -434,12 +460,16 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(120) val ownerUin: Long = 0L,
@JvmField @ProtoNumber(121) val peerUin: Long = 0L,
@JvmField @ProtoNumber(130) val expireTime: Int = 0,
@JvmField @ProtoNumber(140) val fileidcrc: String = "",
@JvmField @ProtoNumber(141) val md5: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(142) val _3sha: ByteArray = EMPTY_BYTE_ARRAY,
) : ProtoBuf
@Serializable
internal class FileQueryReq(
@JvmField @ProtoNumber(10) val uin: Long = 0L,
@JvmField @ProtoNumber(20) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(30) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -453,6 +483,7 @@ internal class Cmd0x346 : ProtoBuf {
internal class RecallFileReq(
@JvmField @ProtoNumber(1) val uin: Long = 0L,
@JvmField @ProtoNumber(2) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(3) val fileidcrc: String = "",
) : ProtoBuf
@Serializable
@ -466,6 +497,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(1) val uin: Long = 0L,
@JvmField @ProtoNumber(2) val beginIndex: Int = 0,
@JvmField @ProtoNumber(3) val reqCount: Int = 0,
@JvmField @ProtoNumber(4) val filterFiletype: Int = 0,
) : ProtoBuf
@Serializable
@ -517,6 +549,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(21) val msgApplyUploadHitReqV3: Cmd0x346.ApplyUploadHitReqV3? = null,
@JvmField @ProtoNumber(101) val businessId: Int = 0,
@JvmField @ProtoNumber(102) val clientType: Int = 0,
@JvmField @ProtoNumber(200) val flagSupportMediaplatform: Int = 0,
@JvmField @ProtoNumber(90000) val msgApplyCopyToReq: Cmd0x346.ApplyCopyToReq? = null,
@JvmField @ProtoNumber(90001) val msgApplyCleanTrafficReq: Cmd0x346.ApplyCleanTrafficReq? = null,
@JvmField @ProtoNumber(90002) val msgApplyGetTrafficReq: Cmd0x346.ApplyGetTrafficReq? = null,
@ -546,6 +579,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(19) val msgApplyUploadRspV3: Cmd0x346.ApplyUploadRspV3? = null,
@JvmField @ProtoNumber(20) val msgApplyUploadHitRspV2: Cmd0x346.ApplyUploadHitRspV2? = null,
@JvmField @ProtoNumber(21) val msgApplyUploadHitRspV3: Cmd0x346.ApplyUploadHitRspV3? = null,
@JvmField @ProtoNumber(50) val flagUseMediaPlatform: Int = 0,
@JvmField @ProtoNumber(90000) val msgApplyCopyToRsp: Cmd0x346.ApplyCopyToRsp? = null,
@JvmField @ProtoNumber(90001) val msgApplyCleanTrafficRsp: Cmd0x346.ApplyCleanTrafficRsp? = null,
@JvmField @ProtoNumber(90002) val msgApplyGetTrafficRsp: Cmd0x346.ApplyGetTrafficRsp? = null,
@ -557,6 +591,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(1) val uin: Long = 0L,
@JvmField @ProtoNumber(2) val beginIndex: Int = 0,
@JvmField @ProtoNumber(3) val reqCount: Int = 0,
@JvmField @ProtoNumber(4) val filterFiletype: Int = 0,
) : ProtoBuf
@Serializable
@ -577,6 +612,7 @@ internal class Cmd0x346 : ProtoBuf {
@JvmField @ProtoNumber(10) val senderUin: Long = 0L,
@JvmField @ProtoNumber(20) val recverUin: Long = 0L,
@JvmField @ProtoNumber(30) val uuid: ByteArray = EMPTY_BYTE_ARRAY,
@JvmField @ProtoNumber(40) val fileidcrc: String = "",
) : ProtoBuf
@Serializable

View File

@ -16,20 +16,21 @@ import net.mamoe.mirai.internal.utils.io.ProtoBuf
import kotlin.jvm.JvmField
@Serializable
internal class GroupFileUploadExt(
internal class FileUploadExt(
@JvmField @ProtoNumber(1) val u1: Int,
@JvmField @ProtoNumber(2) val u2: Int,
@JvmField @ProtoNumber(100) val entry: GroupFileUploadEntry,
@JvmField @ProtoNumber(3) val u3: Int,
@JvmField @ProtoNumber(100) val entry: FileUploadEntry,
@JvmField @ProtoNumber(200) val u200: Int? = null,
) : ProtoBuf
@Serializable
internal class GroupFileUploadEntry(
internal class FileUploadEntry(
@JvmField @ProtoNumber(100) val business: ExcitingBusiInfo,
@JvmField @ProtoNumber(200) val fileEntry: ExcitingFileEntry,
@JvmField @ProtoNumber(300) val clientInfo: ExcitingClientInfo,
@JvmField @ProtoNumber(400) val fileNameInfo: ExcitingFileNameInfo,
@JvmField @ProtoNumber(500) val host: ExcitingHostConfig,
@JvmField @ProtoNumber(500) val host: ExcitingHostConfig? = null,
) : ProtoBuf
@Serializable

View File

@ -181,6 +181,10 @@ internal object KnownPacketFactories {
SummaryCard.ReqSummaryCard,
ChangeFriendRemark,
MusicSharePacket,
OfflineFilleHandleSvr.UploadSucc,
OfflineFilleHandleSvr.ApplyDownload,
OfflineFilleHandleSvr.FileQuery,
OfflineFilleHandleSvr.ApplyUploadV3,
*FileManagement.factories
)

View File

@ -0,0 +1,247 @@
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.packet.chat
import io.ktor.utils.io.core.*
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
internal class OfflineFilleHandleSvr {
internal sealed class FileInfo : Packet {
class Success(
val fileUuid: ByteArray,
val filename: String,
val fileSha1: ByteArray,
val fileMd5: ByteArray,
val fileSize: Long,
val expiryTime: Long,
val ownerUin: Long,
) : FileInfo()
class Failed(val message: String) : FileInfo()
}
internal object UploadSucc : OutgoingPacketFactory<FileInfo>(
"OfflineFilleHandleSvr.pb_ftn_CMD_REQ_UPLOAD_SUCC-800"
) {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): FileInfo {
val resp = readProtoBuf(Cmd0x346.RspBody.serializer())
val upResp = resp.msgUploadSuccRsp
?: return FileInfo.Failed("msgUploadSuccRsp is null")
if (upResp.int32RetCode != 0) {
return FileInfo.Failed("return code is ${upResp.int32RetCode}: ${upResp.retMsg}")
}
val fileInfo = upResp.msgFileInfo
?: return FileInfo.Failed("msgUploadSuccRsp.msgFileInfo is null")
return FileInfo.Success(
fileInfo.uuid,
fileInfo.fileName,
fileInfo._3sha,
fileInfo.md5,
fileInfo.fileSize,
fileInfo.expireTime.toLong(),
fileInfo.ownerUin,
)
}
operator fun invoke(
client: QQAndroidClient,
contact: Contact,
fileUuid: ByteArray,
) = buildOutgoingUniPacket(client) { seq ->
writeProtoBuf(
Cmd0x346.ReqBody.serializer(),
Cmd0x346.ReqBody(
cmd = 800,
seq = seq,
msgUploadSuccReq = Cmd0x346.UploadSuccReq(
senderUin = client.uin,
recverUin = contact.uin,
uuid = fileUuid,
)
)
)
}
}
internal object ApplyDownload : OutgoingPacketFactory<ApplyDownload.Response>(
"OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_DOWNLOAD-1200"
) {
internal sealed class Response : Packet {
class Success(val url: String) : Response()
class Failed(val message: String) : Response()
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp = readProtoBuf(Cmd0x346.RspBody.serializer())
val downResp = resp.msgApplyDownloadRsp
?: return Response.Failed("msgApplyDownloadRsp is null")
if (downResp.int32RetCode != 0) {
return Response.Failed("return code is ${downResp.int32RetCode}: ${downResp.retMsg}")
}
val downInfo = downResp.msgDownloadInfo
?: return Response.Failed("msgDownloadInfo is null")
return Response.Success(buildString {
append("http://")
append(downInfo.downloadDomain)
append(downInfo.downloadUrl)
})
}
operator fun invoke(
client: QQAndroidClient,
fileUuid: ByteArray
) = buildOutgoingUniPacket(client) { seq ->
writeProtoBuf(
Cmd0x346.ReqBody.serializer(),
Cmd0x346.ReqBody(
cmd = 1200,
seq = seq,
businessId = 3,
clientType = 104,
msgApplyDownloadReq = Cmd0x346.ApplyDownloadReq(
uin = client.uin,
uuid = fileUuid,
ownerType = 2
),
msgExtensionReq = Cmd0x346.ExtensionReq(
downloadUrlType = 1
)
)
)
}
}
internal object FileQuery : OutgoingPacketFactory<FileInfo>(
"OfflineFilleHandleSvr.pb_ftn_CMD_REQ_FILE_QUERY-1400"
) {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): FileInfo {
val resp = readProtoBuf(Cmd0x346.RspBody.serializer())
val queryResp = resp.msgFileQueryRsp
?: return FileInfo.Failed("msgFileQueryRsp is null")
if (queryResp.int32RetCode != 0) {
return FileInfo.Failed("return code is ${queryResp.int32RetCode}: ${queryResp.retMsg}")
}
val fileInfo = queryResp.msgFileInfo
?: return FileInfo.Failed("msgFileQueryRsp.msgFileInfo is null")
return FileInfo.Success(
fileInfo.uuid,
fileInfo.fileName,
fileInfo._3sha,
fileInfo.md5,
fileInfo.fileSize,
fileInfo.expireTime.toLong(),
fileInfo.ownerUin,
)
}
operator fun invoke(
client: QQAndroidClient,
fileUuid: ByteArray,
) = buildOutgoingUniPacket(client) { seq ->
writeProtoBuf(
Cmd0x346.ReqBody.serializer(),
Cmd0x346.ReqBody(
cmd = 1400,
seq = seq,
businessId = 3,
clientType = 104,
msgFileQueryReq = Cmd0x346.FileQueryReq(
uin = client.uin,
uuid = fileUuid,
)
)
)
}
}
internal object ApplyUploadV3 : OutgoingPacketFactory<ApplyUploadV3.Response>(
"OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_UPLOAD_V3-1700"
) {
internal sealed class Response : Packet {
class FileExists(val fileUuid: ByteArray,) : Response()
class RequireUpload(val fileUuid: ByteArray, val uploadKey: ByteArray) : Response()
class Failed(val message: String) : Response()
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp = readProtoBuf(Cmd0x346.RspBody.serializer())
val upResp = resp.msgApplyUploadRspV3
?: return Response.Failed("msgApplyUploadRspV3 is null")
if (upResp.int32RetCode != 0) {
return Response.Failed("return code is ${upResp.int32RetCode}: ${upResp.retMsg}")
}
return if (upResp.boolFileExist) {
Response.FileExists(upResp.uuid)
} else {
Response.RequireUpload(upResp.uuid, upResp.mediaPlateformUploadKey)
}
}
operator fun invoke(
client: QQAndroidClient,
contact: Contact,
filename: String,
fileSize: Long,
fileMd5: ByteArray,
fileSha1: ByteArray,
) = buildOutgoingUniPacket(client) { seq ->
writeProtoBuf(
Cmd0x346.ReqBody.serializer(),
Cmd0x346.ReqBody(
cmd = 1700,
seq = seq,
msgApplyUploadReqV3 = Cmd0x346.ApplyUploadReqV3(
senderUin = client.uin,
recverUin = contact.uin,
fileSize = fileSize,
fileName = filename,
_10mMd5 = fileMd5,
sha = fileSha1,
localFilepath = "/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/$filename",
dangerLevel = 0,
totalSpace = 0,
contenttype = 0,
md5 = fileMd5
)
)
)
}
}
}

View File

@ -110,7 +110,7 @@ internal class MessageSerializationTest : AbstractTest() {
MessageOrigin(SimpleServiceMessage(1, "content"), "resource id", MessageOriginKind.LONG),
ShowImageFlag,
Dice(1),
FileMessageImpl("id", 2, "name", 1)
GroupFileMessageImpl("id", 2, "name", 1)
)
@Serializable
@ -120,7 +120,7 @@ internal class MessageSerializationTest : AbstractTest() {
@Test
fun `test FileMessage serialization`() {
val w = W(FileMessageImpl("id", 2, "name", 1))
val w = W(GroupFileMessageImpl("id", 2, "name", 1))
println(w.serialize(W.serializer()))
assertEquals(w, w.serialize(W.serializer()).deserialize(W.serializer()))
}

View File

@ -19,7 +19,7 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.groupCode
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.GroupFileMessageImpl
import net.mamoe.mirai.internal.message.flags.AllowSendFileMessage
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind
@ -446,10 +446,10 @@ internal abstract class CommonRemoteFileImpl(
return resp
}
val ext = GroupFileUploadExt(
val ext = FileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
entry = FileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = bot.id,
@ -486,7 +486,7 @@ internal abstract class CommonRemoteFileImpl(
),
),
u3 = 0,
).toByteArray(GroupFileUploadExt.serializer())
).toByteArray(FileUploadExt.serializer())
callback?.onBegin(this, resource)
@ -519,7 +519,7 @@ internal abstract class CommonRemoteFileImpl(
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val resp = upload0(resource, callback) ?: error("Failed to upload file.")
return FileMessageImpl(
return GroupFileMessageImpl(
resp.fileId, resp.busId, name, resource.size, allowSend = true
)
}
@ -582,7 +582,7 @@ internal abstract class CommonRemoteFileImpl(
override suspend fun toMessage(): FileMessage? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
return FileMessageImpl(info.id, info.busId, name, info.size)
return GroupFileMessageImpl(info.id, info.busId, name, info.size)
}
}