diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index f53f76e59..8d36d5dac 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -91,6 +91,8 @@ public abstract interface class net/mamoe/mirai/IMirai : net/mamoe/mirai/LowLeve public abstract fun createImage (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public synthetic fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)Lkotlin/Unit; public fun deleteGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)V + public fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/MessageChain; + public abstract fun downloadLongMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getBotFactory ()Lnet/mamoe/mirai/BotFactory; public abstract fun getFileCacheStrategy ()Lnet/mamoe/mirai/utils/FileCacheStrategy; public fun getGroupAnnouncement (Lnet/mamoe/mirai/Bot;JLjava/lang/String;)Lnet/mamoe/mirai/data/GroupAnnouncement; @@ -4364,6 +4366,23 @@ public final class net/mamoe/mirai/message/data/LightApp$Key : net/mamoe/mirai/m public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class net/mamoe/mirai/message/data/LongMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata { + public static final field Key Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; + public static synthetic fun copy$default (Lnet/mamoe/mirai/message/data/LongMessageOrigin;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/LongMessageOrigin; + public fun equals (Ljava/lang/Object;)Z + public fun getKey ()Lnet/mamoe/mirai/message/data/LongMessageOrigin$Key; + public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public final fun getResourceId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/message/data/LongMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey { +} + public abstract interface class net/mamoe/mirai/message/data/MarketFace : net/mamoe/mirai/message/data/HummerMessage { public static final field Key Lnet/mamoe/mirai/message/data/MarketFace$Key; public static final field SERIAL_NAME Ljava/lang/String; diff --git a/mirai-core-api/src/commonMain/kotlin/IMirai.kt b/mirai-core-api/src/commonMain/kotlin/IMirai.kt index d694a14cf..0a2764088 100644 --- a/mirai-core-api/src/commonMain/kotlin/IMirai.kt +++ b/mirai-core-api/src/commonMain/kotlin/IMirai.kt @@ -170,6 +170,14 @@ public interface IMirai : LowLevelApiAccessor { originalMessage: MessageChain ): OfflineMessageSource + /** + * @since 2.3 + */ + @JvmBlockingBridge + public suspend fun downloadLongMessage( + bot: Bot, + resourceId: String, + ): MessageChain /** * 通过好友验证 diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt b/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt new file mode 100644 index 000000000..ddc006c1e --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/LongMessageOrigin.kt @@ -0,0 +1,36 @@ +/* + * Copyright 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.message.data + +import net.mamoe.mirai.IMirai +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.safeCast + +/** + * 标识一个长消息. + * + * + * 消息过长后会通过特殊的通道上传和下载, 每条消息都会获得一个 resourceId. + * + * 可以通过 resourceId 下载消息 [IMirai.downloadLongMessage]. + * 但不保证 resourceId 一直有效. + * + * @since 2.3 + */ +@MiraiExperimentalApi +public data class LongMessageOrigin( + val resourceId: String +) : MessageMetadata, ConstrainSingle { + override val key: Key get() = Key + + override fun toString(): String = "" + + public companion object Key : AbstractMessageKey({ it.safeCast() }) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index f86bc7576..f428c76eb 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -16,6 +16,8 @@ import io.ktor.client.request.* import io.ktor.client.request.forms.* import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.withContext +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes import kotlinx.serialization.json.* import net.mamoe.mirai.* import net.mamoe.mirai.contact.* @@ -24,15 +26,18 @@ import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.message.* -import net.mamoe.mirai.internal.network.highway.Highway -import net.mamoe.mirai.internal.network.highway.ResourceKind +import net.mamoe.mirai.internal.network.highway.* import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect import net.mamoe.mirai.internal.network.protocol.packet.summarycard.SummaryCard +import net.mamoe.mirai.internal.utils.crypto.TEA +import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.action.Nudge @@ -959,4 +964,59 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { kind, ids, botId, time, fromId, targetId, originalMessage, internalIds ) + override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain { + bot.asQQAndroidBot() + when (val resp = MultiMsg.ApplyDown(bot.client, 1, resourceId, 1).sendAndExpect(bot)) { + is MultiMsg.ApplyDown.Response.RequireDownload -> { + val http = Mirai.Http + val origin = resp.origin + + val data = if (origin.msgExternInfo?.channelType == 2) { + tryDownload( + bot = bot, + host = "https://ssl.htdata.qq.com", + port = 0, + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { host, port -> + http.get("$host${origin.thumbDownPara}:$port") + } + } else tryServersDownload( + bot = bot, + servers = origin.uint32DownIp.zip(origin.uint32DownPort), + resourceKind = ResourceKind.LONG_MESSAGE, + channelKind = ChannelKind.HTTP + ) { ip, port -> + http.get("http://$ip${origin.thumbDownPara}:$port") + } + + val body = data.read { + check(readByte() == 40.toByte()) { + "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}" + } + val headLength = readInt() + val bodyLength = readInt() + discardExact(headLength) + readBytes(bodyLength) + } + + val decrypted = TEA.decrypt(body, origin.msgKey) + val longResp = + decrypted.loadAs(LongMsg.RspBody.serializer()) + + val down = longResp.msgDownRsp.single() + check(down.result == 0) { + "Message download failed, result=${down.result}, resId=${down.msgResid}, msgContent=${down.msgContent.toUHexString()}" + } + + val content = down.msgContent.ungzip() + val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) + + return transmit.msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP) + } + MultiMsg.ApplyDown.Response.MessageTooLarge -> { + error("Message is too large and cannot download") + } + } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt index 1f26769a3..3c6cc9b40 100644 --- a/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/LongMessageInternal.kt @@ -9,25 +9,11 @@ package net.mamoe.mirai.internal.message -import io.ktor.client.request.* -import kotlinx.io.core.discardExact -import kotlinx.io.core.readBytes import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.asQQAndroidBot -import net.mamoe.mirai.internal.network.highway.ChannelKind -import net.mamoe.mirai.internal.network.highway.ResourceKind -import net.mamoe.mirai.internal.network.highway.tryDownload -import net.mamoe.mirai.internal.network.highway.tryServersDownload -import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit -import net.mamoe.mirai.internal.network.protocol.packet.chat.MultiMsg -import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect -import net.mamoe.mirai.internal.utils.crypto.TEA -import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.safeCast // internal runtime value, not serializable internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) : @@ -36,59 +22,9 @@ internal data class LongMessageInternal internal constructor(override val conten override suspend fun refine(contact: Contact, context: MessageChain): Message { val bot = contact.bot.asQQAndroidBot() - when (val resp = MultiMsg.ApplyDown(bot.client, 1, resId, 1).sendAndExpect(bot)) { - is MultiMsg.ApplyDown.Response.RequireDownload -> { - val http = Mirai.Http - val origin = resp.origin + val long = Mirai.downloadLongMessage(bot, resId) - val data = if (origin.msgExternInfo?.channelType == 2) { - tryDownload( - bot = bot, - host = "https://ssl.htdata.qq.com", - port = 0, - resourceKind = ResourceKind.LONG_MESSAGE, - channelKind = ChannelKind.HTTP - ) { host, port -> - http.get("$host${origin.thumbDownPara}:$port") - } - } else tryServersDownload( - bot = bot, - servers = origin.uint32DownIp.zip(origin.uint32DownPort), - resourceKind = ResourceKind.LONG_MESSAGE, - channelKind = ChannelKind.HTTP - ) { ip, port -> - http.get("http://$ip${origin.thumbDownPara}:$port") - } - - val body = data.read { - check(readByte() == 40.toByte()) { - "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}" - } - val headLength = readInt() - val bodyLength = readInt() - discardExact(headLength) - readBytes(bodyLength) - } - - val decrypted = TEA.decrypt(body, origin.msgKey) - val longResp = - decrypted.loadAs(LongMsg.RspBody.serializer()) - - val down = longResp.msgDownRsp.single() - check(down.result == 0) { - "Message download failed, result=${down.result}, resId=${down.msgResid}, msgContent=${down.msgContent.toUHexString()}" - } - - val content = down.msgContent.ungzip() - val transmit = content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer()) - - val source = context.source - return transmit.msg.toMessageChainNoSource(bot.id, contact.castOrNull()?.id ?: 0, source.kind) - } - MultiMsg.ApplyDown.Response.MessageTooLarge -> { - error("Message is too large and cannot download") - } - } + return LongMessageOrigin(resId) + long } companion object Key :