mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-05 09:22:27 +08:00
Merge pull request #945 from mamoe/parse_long_message
Redesign message transformers and support refining, support long and forward messages
This commit is contained in:
commit
66ef872962
@ -91,6 +91,10 @@ 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 downloadForwardMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;)Ljava/util/List;
|
||||
public abstract fun downloadForwardMessage (Lnet/mamoe/mirai/Bot;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
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;
|
||||
@ -5071,6 +5075,42 @@ public final class net/mamoe/mirai/message/data/RichMessage$Key : net/mamoe/mira
|
||||
public static synthetic fun share$default (Lnet/mamoe/mirai/message/data/RichMessage$Key;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/ServiceMessage;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/RichMessageKind : java/lang/Enum {
|
||||
public static final field FORWARD Lnet/mamoe/mirai/message/data/RichMessageKind;
|
||||
public static final field LONG Lnet/mamoe/mirai/message/data/RichMessageKind;
|
||||
public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RichMessageKind;
|
||||
public static fun values ()[Lnet/mamoe/mirai/message/data/RichMessageKind;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/RichMessageOrigin : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageMetadata {
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key;
|
||||
public synthetic fun <init> (ILnet/mamoe/mirai/message/data/RichMessage;Ljava/lang/String;Lnet/mamoe/mirai/message/data/RichMessageKind;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
|
||||
public fun <init> (Lnet/mamoe/mirai/message/data/RichMessage;Ljava/lang/String;Lnet/mamoe/mirai/message/data/RichMessageKind;)V
|
||||
public fun contentToString ()Ljava/lang/String;
|
||||
public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
|
||||
public fun getKey ()Lnet/mamoe/mirai/message/data/RichMessageOrigin$Key;
|
||||
public final fun getKind ()Lnet/mamoe/mirai/message/data/RichMessageKind;
|
||||
public final fun getOrigin ()Lnet/mamoe/mirai/message/data/RichMessage;
|
||||
public final fun getResourceId ()Ljava/lang/String;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public static final fun write$Self (Lnet/mamoe/mirai/message/data/RichMessageOrigin;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/RichMessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
|
||||
public static final field INSTANCE Lnet/mamoe/mirai/message/data/RichMessageOrigin$$serializer;
|
||||
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
|
||||
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
|
||||
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RichMessageOrigin;
|
||||
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
|
||||
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
|
||||
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/RichMessageOrigin;)V
|
||||
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/RichMessageOrigin$Key : net/mamoe/mirai/message/data/AbstractMessageKey {
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage {
|
||||
public static final field Key Lnet/mamoe/mirai/message/data/ServiceMessage$Key;
|
||||
public fun appendMiraiCodeTo (Ljava/lang/StringBuilder;)V
|
||||
|
@ -170,6 +170,23 @@ public interface IMirai : LowLevelApiAccessor {
|
||||
originalMessage: MessageChain
|
||||
): OfflineMessageSource
|
||||
|
||||
/**
|
||||
* @since 2.3
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public suspend fun downloadLongMessage(
|
||||
bot: Bot,
|
||||
resourceId: String,
|
||||
): MessageChain
|
||||
|
||||
/**
|
||||
* @since 2.3
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public suspend fun downloadForwardMessage(
|
||||
bot: Bot,
|
||||
resourceId: String,
|
||||
): List<ForwardMessage.Node>
|
||||
|
||||
/**
|
||||
* 通过好友验证
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import kotlinx.serialization.Polymorphic
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.IMirai
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.safeCast
|
||||
|
||||
/**
|
||||
* 标识来源 [RichMessage], 存在于接收的 [MessageChain] 中. 在发送消息时会被忽略.
|
||||
*
|
||||
* 一些 [RichMessage] 会被 mirai 解析成特定的更易使用的类型, 如:
|
||||
* - 长消息会被协议内部转化为 [ServiceMessage] `serviceId=35` 通过独立通道上传和下载并获得一个 [resourceId]. mirai 会自动下载长消息并把他们解析为 [MessageChain].
|
||||
* - 合并转发也使用长消息通道传输, 拥有 [resourceId], mirai 解析为 [ForwardMessage]
|
||||
* - [MusicShare] 也有特殊通道上传, 但会作为 [LightApp] 接收.
|
||||
*
|
||||
* 这些经过转换的类型的来源 [RichMessage] 会被包装为 [RichMessageOrigin] 并加入消息链中.
|
||||
*
|
||||
* 如一条被 mirai 解析的长消息的消息链组成为, 第一个元素为 [MessageSource], 第二个元素为 [RichMessageOrigin], 随后为长消息内容.
|
||||
*
|
||||
* 又如一条被 mirai 解析的 [MusicShare] 的消息链组成为, 第一个元素为 [MessageSource], 第二个元素为 [RichMessageOrigin], 第三个元素为 [MusicShare].
|
||||
*
|
||||
* @suppress **注意**: 这是实验性 API: 类名, 类的类型, 构造, 属性等所有 API 均不稳定. 可能会在未来任意时刻变更.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("RichMessageOrigin")
|
||||
@MiraiExperimentalApi("RichMessageOrigin 不稳定")
|
||||
public class RichMessageOrigin(
|
||||
/**
|
||||
* 原 [RichMessage].
|
||||
*/
|
||||
public val origin: @Polymorphic RichMessage,
|
||||
/**
|
||||
* 如果来自长消息或转发消息, 则会有 [resourceId], 否则为 `null`.
|
||||
*
|
||||
* - 下载长消息 [IMirai.downloadLongMessage]
|
||||
* - 下载合并转发消息 [IMirai.downloadForwardMessage]
|
||||
*/
|
||||
public val resourceId: String?,
|
||||
/**
|
||||
* 来源类型
|
||||
*/
|
||||
public val kind: RichMessageKind,
|
||||
) : MessageMetadata, ConstrainSingle {
|
||||
override val key: Key get() = Key
|
||||
|
||||
override fun toString(): String {
|
||||
val resourceId = resourceId
|
||||
return if (resourceId == null) "[mirai:origin:$kind]"
|
||||
else "[mirai:origin:$kind,$resourceId]"
|
||||
}
|
||||
|
||||
override fun contentToString(): String = ""
|
||||
|
||||
public companion object Key : AbstractMessageKey<RichMessageOrigin>({ it.safeCast() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息来源
|
||||
*
|
||||
* @suppress 随着更新, 元素数量会增加. 类名不稳定.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
@MiraiExperimentalApi("RichMessageKind 类名不稳定")
|
||||
public enum class RichMessageKind {
|
||||
/**
|
||||
* 长消息
|
||||
*/
|
||||
LONG,
|
||||
|
||||
/**
|
||||
* 合并转发
|
||||
*/
|
||||
FORWARD,
|
||||
|
||||
// TODO: 2021/2/3 MusicShare RichMessageKind
|
||||
}
|
@ -20,6 +20,13 @@ public inline fun <reified T> Any?.safeCast(): T? = this as? T
|
||||
|
||||
public inline fun <reified T> Any?.castOrNull(): T? = this as? T
|
||||
|
||||
public inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? {
|
||||
for (it in this) {
|
||||
if (it is R) return it
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
|
||||
@kotlin.internal.InlineOnly
|
||||
|
@ -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,80 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
kind, ids, botId, time, fromId, targetId, originalMessage, internalIds
|
||||
)
|
||||
|
||||
override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain {
|
||||
return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.LONG_MESSAGE).msg
|
||||
.toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP)
|
||||
}
|
||||
|
||||
override suspend fun downloadForwardMessage(bot: Bot, resourceId: String): List<ForwardMessage.Node> {
|
||||
return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.FORWARD_MESSAGE).msg.map { msg ->
|
||||
ForwardMessage.Node(
|
||||
senderId = msg.msgHead.fromUin,
|
||||
time = msg.msgHead.msgTime,
|
||||
senderName = msg.msgHead.groupInfo?.groupCard
|
||||
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
|
||||
?: msg.msgHead.fromUin.toString(),
|
||||
messageChain = listOf(msg).toMessageChainNoSource(bot.id, 0, MessageSourceKind.GROUP)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadMultiMsgTransmit(
|
||||
bot: Bot,
|
||||
resourceId: String,
|
||||
resourceKind: ResourceKind,
|
||||
): MsgTransmit.PbMultiMsgTransmit {
|
||||
bot.asQQAndroidBot()
|
||||
when (val resp = MultiMsg.ApplyDown(bot.client, 2, resourceId, 1).sendAndExpect(bot)) {
|
||||
is MultiMsg.ApplyDown.Response.RequireDownload -> {
|
||||
val http = Mirai.Http
|
||||
val origin = resp.origin
|
||||
|
||||
val data: ByteArray = if (origin.msgExternInfo?.channelType == 2) {
|
||||
tryDownload(
|
||||
bot = bot,
|
||||
host = "https://ssl.htdata.qq.com",
|
||||
port = 443,
|
||||
times = 3,
|
||||
resourceKind = resourceKind,
|
||||
channelKind = ChannelKind.HTTP
|
||||
) { host, _ ->
|
||||
http.get("$host${origin.thumbDownPara}")
|
||||
}
|
||||
} else tryServersDownload(
|
||||
bot = bot,
|
||||
servers = origin.uint32DownIp.zip(origin.uint32DownPort),
|
||||
resourceKind = resourceKind,
|
||||
channelKind = ChannelKind.HTTP
|
||||
) { ip, port ->
|
||||
http.get("http://$ip:$port${origin.thumbDownPara}")
|
||||
}
|
||||
|
||||
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()
|
||||
return content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer())
|
||||
}
|
||||
MultiMsg.ApplyDown.Response.MessageTooLarge -> {
|
||||
error("Message is too large and cannot download")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ internal class QQAndroidBot constructor(
|
||||
configuration: BotConfiguration
|
||||
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
|
||||
var client: QQAndroidClient = initClient()
|
||||
private set
|
||||
|
||||
fun initClient(): QQAndroidClient {
|
||||
client = QQAndroidClient(
|
||||
@ -201,5 +202,5 @@ internal fun RichMessage.Key.forwardMessage(
|
||||
<source name="${source.take(50)}" icon="" action="" appid="-1"/>
|
||||
</msg>
|
||||
""".trimIndent().replace("\n", " ")
|
||||
return ForwardMessageInternal(template)
|
||||
return ForwardMessageInternal(template, resId)
|
||||
}
|
@ -23,7 +23,7 @@ import net.mamoe.mirai.internal.network.highway.ChannelKind
|
||||
import net.mamoe.mirai.internal.network.highway.Highway
|
||||
import net.mamoe.mirai.internal.network.highway.ResourceKind.PRIVATE_IMAGE
|
||||
import net.mamoe.mirai.internal.network.highway.postImage
|
||||
import net.mamoe.mirai.internal.network.highway.tryServers
|
||||
import net.mamoe.mirai.internal.network.highway.tryServersUpload
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
|
||||
@ -157,7 +157,7 @@ internal abstract class AbstractUser(
|
||||
)
|
||||
}.recoverCatchingSuppressed {
|
||||
// try upload by http on provided servers
|
||||
tryServers(
|
||||
tryServersUpload(
|
||||
bot = bot,
|
||||
servers = resp.serverIp.zip(resp.serverPort),
|
||||
resourceSize = resource.size,
|
||||
|
@ -48,9 +48,10 @@ internal fun GroupImpl.Companion.checkIsInstance(instance: Group) {
|
||||
check(instance is GroupImpl) { "group is not an instanceof GroupImpl!! DO NOT interlace two or more protocol implementations!!" }
|
||||
}
|
||||
|
||||
internal fun Group.checkIsGroupImpl() {
|
||||
internal fun Group.checkIsGroupImpl(): GroupImpl {
|
||||
contract { returns() implies (this@checkIsGroupImpl is GroupImpl) }
|
||||
GroupImpl.checkIsInstance(this)
|
||||
return this
|
||||
}
|
||||
|
||||
@Suppress("PropertyName")
|
||||
@ -200,7 +201,7 @@ internal class GroupImpl(
|
||||
}.recoverCatchingSuppressed {
|
||||
when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()) {
|
||||
is PttStore.GroupPttUp.Response.RequireUpload -> {
|
||||
tryServers(
|
||||
tryServersUpload(
|
||||
bot,
|
||||
resp.uploadIpList.zip(resp.uploadPortList),
|
||||
resource.size,
|
||||
|
@ -9,24 +9,86 @@
|
||||
|
||||
package net.mamoe.mirai.internal.message
|
||||
|
||||
import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey
|
||||
import net.mamoe.mirai.message.data.AbstractServiceMessage
|
||||
import net.mamoe.mirai.message.data.ServiceMessage
|
||||
import net.mamoe.mirai.Mirai
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.internal.asQQAndroidBot
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.safeCast
|
||||
|
||||
// internal runtime value, not serializable
|
||||
internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) :
|
||||
AbstractServiceMessage() {
|
||||
AbstractServiceMessage(), RefinableMessage {
|
||||
override val serviceId: Int get() = 35
|
||||
|
||||
override suspend fun refine(contact: Contact, context: MessageChain): Message {
|
||||
val bot = contact.bot.asQQAndroidBot()
|
||||
val long = Mirai.downloadLongMessage(bot, resId)
|
||||
|
||||
return RichMessageOrigin(SimpleServiceMessage(serviceId, content), resId, RichMessageKind.LONG) + long
|
||||
}
|
||||
|
||||
companion object Key :
|
||||
AbstractPolymorphicMessageKey<ServiceMessage, LongMessageInternal>(ServiceMessage, { it.safeCast() })
|
||||
}
|
||||
|
||||
// internal runtime value, not serializable
|
||||
internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() {
|
||||
@Suppress("RegExpRedundantEscape", "UnnecessaryVariable")
|
||||
internal data class ForwardMessageInternal(override val content: String, val resId: String) : AbstractServiceMessage(),
|
||||
RefinableMessage {
|
||||
override val serviceId: Int get() = 35
|
||||
|
||||
override suspend fun refine(contact: Contact, context: MessageChain): Message {
|
||||
val bot = contact.bot.asQQAndroidBot()
|
||||
|
||||
val msgXml = content.substringAfter("<msg", "")
|
||||
val xmlHead = msgXml.substringBefore("<item")
|
||||
val xmlFoot: String
|
||||
val xmlContent = msgXml.substringAfter("<item").let {
|
||||
xmlFoot = it.substringAfter("</item", "")
|
||||
it.substringBefore("</item")
|
||||
}
|
||||
val brief = xmlHead.findField("brief")
|
||||
|
||||
val summary = SUMMARY_REGEX.find(xmlContent)?.let { it.groupValues[1] } ?: ""
|
||||
|
||||
val titles = TITLE_REGEX.findAll(xmlContent)
|
||||
.map { it.groupValues[2].trim() }.toMutableList()
|
||||
|
||||
val title = titles.removeFirstOrNull() ?: ""
|
||||
|
||||
val preview = titles
|
||||
val source = xmlFoot.findField("name")
|
||||
|
||||
return RichMessageOrigin(SimpleServiceMessage(serviceId, content), resId, RichMessageKind.FORWARD) + ForwardMessage(
|
||||
preview = preview,
|
||||
title = title,
|
||||
brief = brief,
|
||||
source = source,
|
||||
summary = summary.trim(),
|
||||
nodeList = Mirai.downloadForwardMessage(bot, resId)
|
||||
)
|
||||
}
|
||||
|
||||
companion object Key :
|
||||
AbstractPolymorphicMessageKey<ServiceMessage, ForwardMessageInternal>(ServiceMessage, { it.safeCast() })
|
||||
AbstractPolymorphicMessageKey<ServiceMessage, ForwardMessageInternal>(ServiceMessage, { it.safeCast() }) {
|
||||
|
||||
val SUMMARY_REGEX = """\<summary.*\>(.*?)\<\/summary\>""".toRegex()
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
val TITLE_REGEX = """\<title([A-Za-z\s#\"0-9\=]*)\>([\u0000-\uFFFF]*?)\<\/title\>""".toRegex()
|
||||
|
||||
|
||||
fun String.findField(type: String): String {
|
||||
return substringAfter("$type=\"", "")
|
||||
.substringBefore("\"", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface RefinableMessage : SingleMessage {
|
||||
|
||||
suspend fun refine(
|
||||
contact: Contact,
|
||||
context: MessageChain,
|
||||
): Message?
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.internal.message
|
||||
|
||||
import kotlinx.serialization.Transient
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSource
|
||||
import net.mamoe.mirai.message.data.sourceOrNull
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
internal interface MessageSourceInternal {
|
||||
@Transient
|
||||
val sequenceIds: IntArray // ids
|
||||
|
||||
@Transient
|
||||
val internalIds: IntArray // randomId
|
||||
|
||||
@Deprecated("don't use this internally. Use sequenceId or random instead.", level = DeprecationLevel.ERROR)
|
||||
@Transient
|
||||
val ids: IntArray
|
||||
|
||||
@Transient
|
||||
val isRecalledOrPlanned: AtomicBoolean
|
||||
|
||||
fun toJceData(): ImMsgBody.SourceMsg
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier", "unused")
|
||||
internal suspend fun MessageSource.ensureSequenceIdAvailable() {
|
||||
if (this is OnlineMessageSourceToGroupImpl) {
|
||||
ensureSequenceIdAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier", "unused")
|
||||
internal suspend inline fun Message.ensureSequenceIdAvailable() {
|
||||
(this as? MessageChain)?.sourceOrNull?.ensureSequenceIdAvailable()
|
||||
}
|
@ -0,0 +1,443 @@
|
||||
/*
|
||||
* 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.internal.message
|
||||
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements
|
||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain
|
||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toVoice
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFace
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||
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.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
internal fun ImMsgBody.SourceMsg.toMessageChainNoSource(
|
||||
botId: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
groupIdOrZero: Long
|
||||
): MessageChain {
|
||||
val elements = this.elems
|
||||
return buildMessageChain(elements.size + 1) {
|
||||
joinToMessageChain(elements, groupIdOrZero, messageSourceKind, botId, this)
|
||||
}.cleanupRubbishMessageElements()
|
||||
}
|
||||
|
||||
|
||||
internal fun List<MsgComm.Msg>.toMessageChainOnline(
|
||||
bot: Bot,
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain {
|
||||
return toMessageChain(bot, bot.id, groupIdOrZero, true, messageSourceKind)
|
||||
}
|
||||
|
||||
internal fun List<MsgComm.Msg>.toMessageChainOffline(
|
||||
bot: Bot,
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain {
|
||||
return toMessageChain(bot, bot.id, groupIdOrZero, false, messageSourceKind)
|
||||
}
|
||||
|
||||
internal fun List<MsgComm.Msg>.toMessageChainNoSource(
|
||||
botId: Long,
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain {
|
||||
return toMessageChain(null, botId, groupIdOrZero, null, messageSourceKind)
|
||||
}
|
||||
|
||||
private fun List<MsgComm.Msg>.toMessageChain(
|
||||
bot: Bot?,
|
||||
botId: Long,
|
||||
groupIdOrZero: Long,
|
||||
onlineSource: Boolean?,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain {
|
||||
val messageList = this
|
||||
|
||||
|
||||
val elements = messageList.flatMap { it.msgBody.richText.elems }
|
||||
|
||||
val builder = MessageChainBuilder(elements.size)
|
||||
|
||||
if (onlineSource != null) {
|
||||
checkNotNull(bot)
|
||||
builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList))
|
||||
}
|
||||
|
||||
joinToMessageChain(elements, groupIdOrZero, messageSourceKind, botId, builder)
|
||||
|
||||
for (msg in messageList) {
|
||||
msg.msgBody.richText.ptt?.toVoice()?.let { builder.add(it) }
|
||||
}
|
||||
|
||||
return builder.build().cleanupRubbishMessageElements()
|
||||
}
|
||||
|
||||
private object ReceiveMessageTransformer {
|
||||
fun createMessageSource(
|
||||
bot: Bot,
|
||||
onlineSource: Boolean,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
messageList: List<MsgComm.Msg>,
|
||||
): MessageSource {
|
||||
return when (onlineSource) {
|
||||
true -> {
|
||||
when (messageSourceKind) {
|
||||
MessageSourceKind.TEMP -> OnlineMessageSourceFromTempImpl(bot, messageList)
|
||||
MessageSourceKind.GROUP -> OnlineMessageSourceFromGroupImpl(bot, messageList)
|
||||
MessageSourceKind.FRIEND -> OnlineMessageSourceFromFriendImpl(bot, messageList)
|
||||
MessageSourceKind.STRANGER -> OnlineMessageSourceFromStrangerImpl(bot, messageList)
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
OfflineMessageSourceImplData(bot, messageList, messageSourceKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun joinToMessageChain(
|
||||
elements: List<ImMsgBody.Elem>,
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
botId: Long,
|
||||
builder: MessageChainBuilder
|
||||
) {
|
||||
// (this._miraiContentToString().soutv())
|
||||
for (element in elements) {
|
||||
transformElement(element, groupIdOrZero, messageSourceKind, botId, builder)
|
||||
when {
|
||||
element.richMsg != null -> decodeRichMessage(element.richMsg, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformElement(
|
||||
element: ImMsgBody.Elem,
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
botId: Long,
|
||||
builder: MessageChainBuilder
|
||||
) {
|
||||
when {
|
||||
element.srcMsg != null -> decodeSrcMsg(element.srcMsg, builder, botId, messageSourceKind, groupIdOrZero)
|
||||
element.notOnlineImage != null -> builder.add(OnlineFriendImageImpl(element.notOnlineImage))
|
||||
element.customFace != null -> decodeCustomFace(element.customFace, builder)
|
||||
element.face != null -> builder.add(Face(element.face.index))
|
||||
element.text != null -> decodeText(element.text, builder)
|
||||
element.marketFace != null -> builder.add(MarketFaceImpl(element.marketFace))
|
||||
element.lightApp != null -> decodeLightApp(element.lightApp, builder)
|
||||
element.customElem != null -> decodeCustomElem(element.customElem, builder)
|
||||
element.commonElem != null -> decodeCommonElem(element.commonElem, builder)
|
||||
|
||||
element.elemFlags2 != null
|
||||
|| element.extraInfo != null
|
||||
|| element.generalFlags != null -> {
|
||||
// ignore
|
||||
}
|
||||
else -> {
|
||||
// println(it._miraiContentToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
|
||||
var previousLast: SingleMessage? = null
|
||||
var last: SingleMessage? = null
|
||||
return buildMessageChain(initialSize = this.count()) {
|
||||
this@cleanupRubbishMessageElements.forEach { element ->
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
if (last is LongMessageInternal && element is PlainText) {
|
||||
if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
if (last is PokeMessage && element is PlainText) {
|
||||
if (element == UNSUPPORTED_POKE_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
if (last is VipFace && element is PlainText) {
|
||||
val l = last as VipFace
|
||||
if (element.content.length == 4 + (l.count / 10) + l.kind.name.length) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
// 解决tim发送的语音无法正常识别
|
||||
if (element is PlainText) {
|
||||
if (element == UNSUPPORTED_VOICE_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
if (element is PlainText && last is At && previousLast is QuoteReply
|
||||
&& element.content.startsWith(' ')
|
||||
) {
|
||||
// Android QQ 发送, 是 Quote+At+PlainText(" xxx") // 首空格
|
||||
removeLastOrNull() // At
|
||||
val new = PlainText(element.content.substring(1))
|
||||
add(new)
|
||||
previousLast = null
|
||||
last = new
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (element is QuoteReply) {
|
||||
// 客户端为兼容早期不支持 QuoteReply 的客户端而添加的 At
|
||||
removeLastOrNull()?.let { rm ->
|
||||
if ((rm as? PlainText)?.content != " ") add(rm)
|
||||
else removeLastOrNull()?.let { rm2 ->
|
||||
if (rm2 !is At) add(rm2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element is PlainText) { // 处理分片消息
|
||||
append(element.content)
|
||||
} else {
|
||||
add(element)
|
||||
}
|
||||
|
||||
previousLast = last
|
||||
last = element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) {
|
||||
if (text.attr6Buf.isEmpty()) {
|
||||
list.add(PlainText(text.str))
|
||||
} else {
|
||||
val id: Long
|
||||
text.attr6Buf.read {
|
||||
discardExact(7)
|
||||
id = readUInt().toLong()
|
||||
}
|
||||
if (id == 0L) {
|
||||
list.add(AtAll)
|
||||
} else {
|
||||
list.add(At(id)) // element.text.str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeSrcMsg(
|
||||
srcMsg: ImMsgBody.SourceMsg,
|
||||
list: MessageChainBuilder,
|
||||
botId: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
groupIdOrZero: Long
|
||||
) {
|
||||
list.add(QuoteReply(OfflineMessageSourceImplData(srcMsg, botId, messageSourceKind, groupIdOrZero)))
|
||||
}
|
||||
|
||||
private fun decodeCustomFace(
|
||||
customFace: ImMsgBody.CustomFace,
|
||||
builder: MessageChainBuilder,
|
||||
) {
|
||||
builder.add(OnlineGroupImageImpl(customFace))
|
||||
customFace.pbReserve.let {
|
||||
if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) {
|
||||
builder.add(ShowImageFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeLightApp(
|
||||
lightApp: ImMsgBody.LightAppElem,
|
||||
list: MessageChainBuilder
|
||||
) {
|
||||
val content = runWithBugReport("解析 lightApp",
|
||||
{ "resId=" + lightApp.msgResid + "data=" + lightApp.data.toUHexString() }) {
|
||||
when (lightApp.data[0].toInt()) {
|
||||
0 -> lightApp.data.encodeToString(offset = 1)
|
||||
1 -> lightApp.data.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${lightApp.data[0]}")
|
||||
}
|
||||
}
|
||||
|
||||
list.add(LightApp(content).refine())
|
||||
}
|
||||
|
||||
private fun decodeCustomElem(
|
||||
customElem: ImMsgBody.CustomElem,
|
||||
list: MessageChainBuilder
|
||||
) {
|
||||
customElem.data.read {
|
||||
kotlin.runCatching {
|
||||
CustomMessage.load(this)
|
||||
}.fold(
|
||||
onFailure = {
|
||||
if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) {
|
||||
throw IllegalStateException(
|
||||
"Internal error: " +
|
||||
"exception while deserializing CustomMessage head data," +
|
||||
" data=${customElem.data.toUHexString()}", it
|
||||
)
|
||||
} else {
|
||||
it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException
|
||||
throw IllegalStateException(
|
||||
"User error: " +
|
||||
"exception while deserializing CustomMessage body," +
|
||||
" body=${it.body.toUHexString()}", it
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
onSuccess = {
|
||||
if (it != null) {
|
||||
list.add(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeCommonElem(
|
||||
commonElem: ImMsgBody.CommonElem,
|
||||
list: MessageChainBuilder
|
||||
) {
|
||||
when (commonElem.serviceType) {
|
||||
23 -> {
|
||||
val proto =
|
||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer())
|
||||
list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount))
|
||||
}
|
||||
2 -> {
|
||||
val proto =
|
||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||
list.add(PokeMessage(
|
||||
proto.vaspokeName.takeIf { it.isNotEmpty() }
|
||||
?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name
|
||||
.orEmpty(),
|
||||
proto.pokeType,
|
||||
proto.vaspokeId
|
||||
)
|
||||
)
|
||||
}
|
||||
3 -> {
|
||||
val proto =
|
||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())
|
||||
if (proto.flashTroopPic != null) {
|
||||
list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic)))
|
||||
}
|
||||
if (proto.flashC2cPic != null) {
|
||||
list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic)))
|
||||
}
|
||||
}
|
||||
33 -> {
|
||||
val proto =
|
||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer())
|
||||
list.add(Face(proto.index))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeRichMessage(
|
||||
richMsg: ImMsgBody.RichMsg,
|
||||
builder: MessageChainBuilder
|
||||
) {
|
||||
val content = runWithBugReport("解析 richMsg", { richMsg.template1.toUHexString() }) {
|
||||
when (richMsg.template1[0].toInt()) {
|
||||
0 -> richMsg.template1.encodeToString(offset = 1)
|
||||
1 -> richMsg.template1.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${richMsg.template1[0]}")
|
||||
}
|
||||
}
|
||||
when (richMsg.serviceId) {
|
||||
// 5: 使用微博长图转换功能分享到QQ群
|
||||
/*
|
||||
<?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
|
||||
阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
|
||||
30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
|
||||
浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
|
||||
="" a_actionData="" url=""/></msg>
|
||||
*/
|
||||
/**
|
||||
* json?
|
||||
*/
|
||||
1 -> @Suppress("DEPRECATION_ERROR")
|
||||
builder.add(SimpleServiceMessage(1, content))
|
||||
/**
|
||||
* [LongMessageInternal], [ForwardMessage]
|
||||
*/
|
||||
35 -> {
|
||||
fun findStringProperty(name: String): String {
|
||||
return content.substringAfter("$name=\"", "").substringBefore("\"", "")
|
||||
}
|
||||
|
||||
val resId = findStringProperty("m_resid")
|
||||
|
||||
val msg = if (resId.isEmpty()) {
|
||||
SimpleServiceMessage(35, content)
|
||||
} else when (findStringProperty("multiMsgFlag").toIntOrNull()) {
|
||||
1 -> LongMessageInternal(content, resId)
|
||||
0 -> ForwardMessageInternal(content, resId)
|
||||
else -> {
|
||||
// from PC QQ
|
||||
if (findStringProperty("action") == "viewMultiMsg") {
|
||||
ForwardMessageInternal(content, resId)
|
||||
} else {
|
||||
SimpleServiceMessage(35, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.add(msg)
|
||||
}
|
||||
|
||||
// 104 新群员入群的消息
|
||||
else -> {
|
||||
builder.add(SimpleServiceMessage(richMsg.serviceId, content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ImMsgBody.Ptt.toVoice() = Voice(
|
||||
kotlinx.io.core.String(fileName),
|
||||
fileMd5,
|
||||
fileSize.toLong(),
|
||||
format,
|
||||
kotlinx.io.core.String(downPara)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 [ForwardMessageInternal], [LongMessageInternal]
|
||||
*/
|
||||
internal suspend fun MessageChain.refine(contact: Contact): MessageChain {
|
||||
if (none { it is RefinableMessage }) return this
|
||||
val builder = MessageChainBuilder(this.size)
|
||||
for (singleMessage in this) {
|
||||
if (singleMessage is RefinableMessage) {
|
||||
val v = singleMessage.refine(contact, this)
|
||||
if (v != null) builder.add(v)
|
||||
} else {
|
||||
builder.add(singleMessage)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.internal.message
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
|
||||
internal fun contextualBugReportException(
|
||||
context: String,
|
||||
forDebug: String,
|
||||
e: Throwable? = null,
|
||||
additional: String = ""
|
||||
): IllegalStateException {
|
||||
return IllegalStateException(
|
||||
"在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
|
||||
@kotlin.internal.InlineOnly
|
||||
internal inline fun <R> runWithBugReport(context: String, forDebug: () -> String, block: () -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
callsInPlace(forDebug, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
|
||||
return runCatching(block).getOrElse {
|
||||
throw contextualBugReportException(context, forDebug(), it)
|
||||
}
|
||||
}
|
@ -1,673 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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:OptIn(LowLevelApi::class)
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR")
|
||||
|
||||
package net.mamoe.mirai.internal.message
|
||||
|
||||
import kotlinx.io.core.String
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import kotlinx.io.core.toByteArray
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.LowLevelApi
|
||||
import net.mamoe.mirai.contact.AnonymousMember
|
||||
import net.mamoe.mirai.contact.ContactOrBot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.User
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.*
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.internal.utils.*
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
|
||||
private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
|
||||
private val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。")
|
||||
private val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。")
|
||||
private val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com")
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
internal fun MessageChain.toRichTextElems(
|
||||
messageTarget: ContactOrBot?,
|
||||
withGeneralFlags: Boolean
|
||||
): MutableList<ImMsgBody.Elem> {
|
||||
val forGroup = messageTarget is Group
|
||||
val elements = ArrayList<ImMsgBody.Elem>(this.size)
|
||||
|
||||
if (this.anyIsInstance<QuoteReply>()) {
|
||||
when (val source = this[QuoteReply]!!.source) {
|
||||
is MessageSourceInternal -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
|
||||
else -> error("unsupported MessageSource implementation: ${source::class.simpleName}. Don't implement your own MessageSource.")
|
||||
}
|
||||
}
|
||||
|
||||
var longTextResId: String? = null
|
||||
|
||||
fun transformOneMessage(currentMessage: Message) {
|
||||
if (currentMessage is RichMessage) {
|
||||
val content = currentMessage.content.toByteArray().zip()
|
||||
when (currentMessage) {
|
||||
is ForwardMessageInternal -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = currentMessage.serviceId, // ok
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||
}
|
||||
is LongMessageInternal -> {
|
||||
check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = currentMessage.serviceId, // ok
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||
longTextResId = currentMessage.resId
|
||||
}
|
||||
is LightApp -> elements.add(
|
||||
ImMsgBody.Elem(
|
||||
lightApp = ImMsgBody.LightAppElem(
|
||||
data = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = when (currentMessage) {
|
||||
is ServiceMessage -> currentMessage.serviceId
|
||||
else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}")
|
||||
},
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (currentMessage) {
|
||||
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content)))
|
||||
is CustomMessage -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
customElem = ImMsgBody.CustomElem(
|
||||
enumType = MIRAI_CUSTOM_ELEM_TYPE,
|
||||
data = CustomMessage.dump(
|
||||
currentMessage.getFactory() as CustomMessage.Factory<CustomMessage>,
|
||||
currentMessage
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is At -> {
|
||||
elements.add(ImMsgBody.Elem(text = currentMessage.toJceData(messageTarget.safeCast())))
|
||||
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
|
||||
// removed by https://github.com/mamoe/mirai/issues/524
|
||||
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||
}
|
||||
is PokeMessage -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
commonElem = ImMsgBody.CommonElem(
|
||||
serviceType = 2,
|
||||
businessType = currentMessage.pokeType,
|
||||
pbElem = HummerCommelem.MsgElemInfoServtype2(
|
||||
pokeType = currentMessage.pokeType,
|
||||
vaspokeId = currentMessage.id,
|
||||
vaspokeMinver = "7.2.0",
|
||||
vaspokeName = currentMessage.name
|
||||
).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN)
|
||||
}
|
||||
|
||||
|
||||
is OfflineGroupImage -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData()))
|
||||
}
|
||||
}
|
||||
is OnlineGroupImageImpl -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate))
|
||||
}
|
||||
}
|
||||
is OnlineFriendImageImpl -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace()))
|
||||
}
|
||||
}
|
||||
is OfflineFriendImage -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
is FlashImage -> elements.add(currentMessage.toJceData(messageTarget))
|
||||
.also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
|
||||
|
||||
|
||||
is AtAll -> elements.add(atAllData)
|
||||
is Face -> elements.add(
|
||||
if (currentMessage.id >= 260) {
|
||||
ImMsgBody.Elem(commonElem = currentMessage.toCommData())
|
||||
} else {
|
||||
ImMsgBody.Elem(face = currentMessage.toJceData())
|
||||
}
|
||||
)
|
||||
is QuoteReply -> {
|
||||
if (forGroup) {
|
||||
when (val source = currentMessage.source) {
|
||||
is OnlineMessageSource.Incoming.FromGroup -> {
|
||||
val sender0 = source.sender
|
||||
if (sender0 !is AnonymousMember)
|
||||
transformOneMessage(At(sender0))
|
||||
// transformOneMessage(PlainText(" "))
|
||||
// removed by https://github.com/mamoe/mirai/issues/524
|
||||
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is MarketFace -> {
|
||||
if (currentMessage is MarketFaceImpl) {
|
||||
elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate))
|
||||
}
|
||||
//兼容信息
|
||||
transformOneMessage(PlainText(currentMessage.name))
|
||||
if (currentMessage is MarketFaceImpl) {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString()))
|
||||
is PttMessage -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1)
|
||||
)
|
||||
)
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
elemFlags2 = ImMsgBody.ElemFlags2(
|
||||
vipStatus = 1
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is MusicShare -> {
|
||||
// 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
|
||||
// 发送消息时会被特殊处理
|
||||
transformOneMessage(PlainText(currentMessage.content))
|
||||
}
|
||||
|
||||
is ForwardMessage,
|
||||
is MessageSource, // mirai metadata only
|
||||
is RichMessage // already transformed above
|
||||
-> {
|
||||
|
||||
}
|
||||
is InternalFlagOnlyMessage, is ShowImageFlag -> {
|
||||
// ignore
|
||||
}
|
||||
else -> error("unsupported message type: ${currentMessage::class.simpleName}")
|
||||
}
|
||||
}
|
||||
this.forEach(::transformOneMessage)
|
||||
|
||||
if (withGeneralFlags) {
|
||||
when {
|
||||
longTextResId != null -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
generalFlags = ImMsgBody.GeneralFlags(
|
||||
longTextFlag = 1,
|
||||
longTextResid = longTextResId!!,
|
||||
pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
this.anyIsInstance<MarketFaceImpl>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE)))
|
||||
}
|
||||
this.anyIsInstance<RichMessage>() -> {
|
||||
// 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE)))
|
||||
}
|
||||
this.anyIsInstance<FlashImage>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU)))
|
||||
}
|
||||
this.anyIsInstance<PttMessage>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT)))
|
||||
}
|
||||
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE)))
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
private val PB_RESERVE_FOR_RICH_MESSAGE =
|
||||
"08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
|
||||
|
||||
private val PB_RESERVE_FOR_PTT =
|
||||
"78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes()
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes()
|
||||
private val PB_RESERVE_FOR_MARKET_FACE =
|
||||
"02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes()
|
||||
private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
||||
|
||||
internal fun MsgComm.Msg.toMessageChain(
|
||||
bot: Bot,
|
||||
groupIdOrZero: Long,
|
||||
onlineSource: Boolean,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain = listOf(this).toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, messageSourceKind)
|
||||
|
||||
internal fun List<MsgOnlinePush.PbPushMsg>.toMessageChain(
|
||||
bot: Bot,
|
||||
groupIdOrZero: Long,
|
||||
onlineSource: Boolean,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain = map { it.msg }.toMessageChain(bot, bot.id, groupIdOrZero, onlineSource, messageSourceKind)
|
||||
|
||||
internal fun List<MsgComm.Msg>.toMessageChain(
|
||||
bot: Bot?,
|
||||
botId: Long,
|
||||
groupIdOrZero: Long,
|
||||
onlineSource: Boolean?,
|
||||
messageSourceKind: MessageSourceKind
|
||||
): MessageChain {
|
||||
val elements = this.flatMap { it.msgBody.richText.elems }
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
val ptts = buildList<Message> {
|
||||
this@toMessageChain.forEach { msg ->
|
||||
msg.msgBody.richText.ptt?.run {
|
||||
// when (fileType) {
|
||||
// 4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
|
||||
// else -> null
|
||||
// }
|
||||
add(Voice(String(fileName), fileMd5, fileSize.toLong(), format, String(downPara)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildMessageChain(elements.size + 1 + ptts.size) {
|
||||
when (onlineSource) {
|
||||
true -> {
|
||||
checkNotNull(bot) { "bot is null" }
|
||||
|
||||
when (messageSourceKind) {
|
||||
MessageSourceKind.TEMP -> +OnlineMessageSourceFromTempImpl(bot, this@toMessageChain)
|
||||
MessageSourceKind.GROUP -> +OnlineMessageSourceFromGroupImpl(bot, this@toMessageChain)
|
||||
MessageSourceKind.FRIEND -> +OnlineMessageSourceFromFriendImpl(bot, this@toMessageChain)
|
||||
MessageSourceKind.STRANGER -> +OnlineMessageSourceFromStrangerImpl(bot, this@toMessageChain)
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
+OfflineMessageSourceImplData(bot, this@toMessageChain, botId)
|
||||
}
|
||||
null -> {
|
||||
|
||||
}
|
||||
}
|
||||
elements.joinToMessageChain(groupIdOrZero, messageSourceKind, botId, this)
|
||||
addAll(ptts)
|
||||
}.cleanupRubbishMessageElements()
|
||||
}
|
||||
|
||||
// These two functions have difference method signature, don't combine.
|
||||
|
||||
internal fun ImMsgBody.SourceMsg.toMessageChain(
|
||||
botId: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
groupIdOrZero: Long
|
||||
): MessageChain {
|
||||
val elements = this.elems
|
||||
if (elements.isEmpty())
|
||||
error("elements for SourceMsg is empty")
|
||||
return buildMessageChain(elements.size + 1) {
|
||||
/*
|
||||
+OfflineMessageSourceImplData(
|
||||
delegate = this@toMessageChain,
|
||||
botId = botId,
|
||||
messageSourceKind = messageSourceKind,
|
||||
groupIdOrZero = groupIdOrZero
|
||||
)*/
|
||||
elements.joinToMessageChain(groupIdOrZero, messageSourceKind, botId, this)
|
||||
}.cleanupRubbishMessageElements()
|
||||
}
|
||||
|
||||
private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
|
||||
var previousLast: SingleMessage? = null
|
||||
var last: SingleMessage? = null
|
||||
return buildMessageChain(initialSize = this.count()) {
|
||||
this@cleanupRubbishMessageElements.forEach { element ->
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
if (last is LongMessageInternal && element is PlainText) {
|
||||
if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
if (last is PokeMessage && element is PlainText) {
|
||||
if (element == UNSUPPORTED_POKE_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
if (last is VipFace && element is PlainText) {
|
||||
val l = last as VipFace
|
||||
if (element.content.length == 4 + (l.count / 10) + l.kind.name.length) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
// 解决tim发送的语音无法正常识别
|
||||
if (element is PlainText) {
|
||||
if (element == UNSUPPORTED_VOICE_MESSAGE_PLAIN) {
|
||||
previousLast = last
|
||||
last = element
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
if (element is PlainText && last is At && previousLast is QuoteReply
|
||||
&& element.content.startsWith(' ')
|
||||
) {
|
||||
// Android QQ 发送, 是 Quote+At+PlainText(" xxx") // 首空格
|
||||
removeLastOrNull() // At
|
||||
val new = PlainText(element.content.substring(1))
|
||||
add(new)
|
||||
previousLast = null
|
||||
last = new
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (element is QuoteReply) {
|
||||
// 客户端为兼容早期不支持 QuoteReply 的客户端而添加的 At
|
||||
removeLastOrNull()?.let { rm ->
|
||||
if ((rm as? PlainText)?.content != " ") add(rm)
|
||||
else removeLastOrNull()?.let { rm2 ->
|
||||
if (rm2 !is At) add(rm2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element is PlainText) { // 处理分片消息
|
||||
append(element.content)
|
||||
} else {
|
||||
add(element)
|
||||
}
|
||||
|
||||
previousLast = last
|
||||
last = element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? {
|
||||
for (it in this) {
|
||||
if (it is R) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510
|
||||
|
||||
internal fun List<ImMsgBody.Elem>.joinToMessageChain(
|
||||
groupIdOrZero: Long,
|
||||
messageSourceKind: MessageSourceKind,
|
||||
botId: Long,
|
||||
list: MessageChainBuilder
|
||||
) {
|
||||
// (this._miraiContentToString().soutv())
|
||||
var marketFace: MarketFaceImpl? = null
|
||||
this.forEach { element ->
|
||||
when {
|
||||
element.srcMsg != null -> {
|
||||
list.add(
|
||||
QuoteReply(
|
||||
OfflineMessageSourceImplData(
|
||||
element.srcMsg,
|
||||
botId,
|
||||
messageSourceKind,
|
||||
groupIdOrZero
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
element.notOnlineImage != null -> list.add(OnlineFriendImageImpl(element.notOnlineImage))
|
||||
element.customFace != null -> {
|
||||
list.add(OnlineGroupImageImpl(element.customFace))
|
||||
element.customFace.pbReserve.let {
|
||||
if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) {
|
||||
list.add(ShowImageFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
element.face != null -> list.add(Face(element.face.index))
|
||||
element.text != null -> {
|
||||
if (element.text.attr6Buf.isEmpty()) {
|
||||
if (marketFace != null && marketFace!!.name.isEmpty()) {
|
||||
marketFace!!.delegate.faceName = element.text.str.toByteArray()
|
||||
} else {
|
||||
list.add(PlainText(element.text.str))
|
||||
}
|
||||
} else {
|
||||
val id: Long
|
||||
element.text.attr6Buf.read {
|
||||
discardExact(7)
|
||||
id = readUInt().toLong()
|
||||
}
|
||||
if (id == 0L) {
|
||||
list.add(AtAll)
|
||||
} else {
|
||||
list.add(At(id)) // element.text.str
|
||||
}
|
||||
}
|
||||
}
|
||||
element.marketFace != null -> {
|
||||
list.add(MarketFaceImpl(element.marketFace).also {
|
||||
marketFace = it
|
||||
})
|
||||
}
|
||||
element.lightApp != null -> {
|
||||
val content = runWithBugReport("解析 lightApp",
|
||||
{ "resId=" + element.lightApp.msgResid + "data=" + element.lightApp.data.toUHexString() }) {
|
||||
when (element.lightApp.data[0].toInt()) {
|
||||
0 -> element.lightApp.data.encodeToString(offset = 1)
|
||||
1 -> element.lightApp.data.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${element.lightApp.data[0]}")
|
||||
}
|
||||
}
|
||||
|
||||
list.add(LightApp(content).refine())
|
||||
}
|
||||
element.richMsg != null -> {
|
||||
val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {
|
||||
when (element.richMsg.template1[0].toInt()) {
|
||||
0 -> element.richMsg.template1.encodeToString(offset = 1)
|
||||
1 -> element.richMsg.template1.unzip(1).encodeToString()
|
||||
else -> error("unknown compression flag=${element.richMsg.template1[0]}")
|
||||
}
|
||||
}
|
||||
when (element.richMsg.serviceId) {
|
||||
// 5: 使用微博长图转换功能分享到QQ群
|
||||
/*
|
||||
<?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
|
||||
阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
|
||||
30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
|
||||
浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
|
||||
="" a_actionData="" url=""/></msg>
|
||||
*/
|
||||
/**
|
||||
* json?
|
||||
*/
|
||||
1 -> @Suppress("DEPRECATION_ERROR")
|
||||
list.add(SimpleServiceMessage(1, content))
|
||||
/**
|
||||
* [LongMessageInternal], [ForwardMessage]
|
||||
*/
|
||||
35 -> {
|
||||
val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.longTextResid
|
||||
|
||||
if (resId != null) {
|
||||
// TODO: 2020/4/29 解析长消息
|
||||
list.add(SimpleServiceMessage(35, content)) // resId
|
||||
} else {
|
||||
// TODO: 2020/4/29 解析合并转发
|
||||
list.add(SimpleServiceMessage(35, content))
|
||||
}
|
||||
}
|
||||
|
||||
// 104 新群员入群的消息
|
||||
else -> {
|
||||
list.add(SimpleServiceMessage(element.richMsg.serviceId, content))
|
||||
}
|
||||
}
|
||||
}
|
||||
element.elemFlags2 != null
|
||||
|| element.extraInfo != null
|
||||
|| element.generalFlags != null -> {
|
||||
|
||||
}
|
||||
element.customElem != null -> {
|
||||
element.customElem.data.read {
|
||||
kotlin.runCatching {
|
||||
CustomMessage.load(this)
|
||||
}.fold(
|
||||
onFailure = {
|
||||
if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) {
|
||||
throw IllegalStateException(
|
||||
"Internal error: " +
|
||||
"exception while deserializing CustomMessage head data," +
|
||||
" data=${element.customElem.data.toUHexString()}", it
|
||||
)
|
||||
} else {
|
||||
it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException
|
||||
throw IllegalStateException(
|
||||
"User error: " +
|
||||
"exception while deserializing CustomMessage body," +
|
||||
" body=${it.body.toUHexString()}", it
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
onSuccess = {
|
||||
if (it != null) {
|
||||
list.add(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
element.commonElem != null -> {
|
||||
when (element.commonElem.serviceType) {
|
||||
23 -> {
|
||||
val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer())
|
||||
list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount))
|
||||
}
|
||||
2 -> {
|
||||
val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||
list.add(PokeMessage(
|
||||
proto.vaspokeName.takeIf { it.isNotEmpty() }
|
||||
?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name
|
||||
.orEmpty(),
|
||||
proto.pokeType,
|
||||
proto.vaspokeId
|
||||
)
|
||||
)
|
||||
}
|
||||
3 -> {
|
||||
val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())
|
||||
if (proto.flashTroopPic != null) {
|
||||
list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic)))
|
||||
}
|
||||
if (proto.flashC2cPic != null) {
|
||||
list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic)))
|
||||
}
|
||||
}
|
||||
33 -> {
|
||||
val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer())
|
||||
list.add(Face(proto.index))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// println(it._miraiContentToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
internal fun contextualBugReportException(
|
||||
context: String,
|
||||
forDebug: String,
|
||||
e: Throwable? = null,
|
||||
additional: String = ""
|
||||
): IllegalStateException {
|
||||
return IllegalStateException("在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", e)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE")
|
||||
@kotlin.internal.InlineOnly
|
||||
internal inline fun <R> runWithBugReport(context: String, forDebug: () -> String, block: () -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
callsInPlace(forDebug, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
|
||||
return runCatching(block).getOrElse {
|
||||
throw contextualBugReportException(context, forDebug(), it)
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.contact.Stranger
|
||||
import net.mamoe.mirai.internal.contact.GroupImpl
|
||||
import net.mamoe.mirai.internal.contact.checkIsGroupImpl
|
||||
import net.mamoe.mirai.internal.contact.newAnonymous
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
|
||||
@ -25,66 +25,32 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.internal.utils._miraiContentToString
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.utils.encodeToBase64
|
||||
import net.mamoe.mirai.utils.encodeToString
|
||||
import net.mamoe.mirai.utils.mapToIntArray
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
internal interface MessageSourceInternal {
|
||||
@Transient
|
||||
val sequenceIds: IntArray // ids
|
||||
|
||||
@Transient
|
||||
val internalIds: IntArray // randomId
|
||||
|
||||
@Deprecated("don't use this internally. Use sequenceId or random instead.", level = DeprecationLevel.ERROR)
|
||||
@Transient
|
||||
val ids: IntArray
|
||||
|
||||
@Transient
|
||||
val isRecalledOrPlanned: AtomicBoolean
|
||||
|
||||
fun toJceData(): ImMsgBody.SourceMsg
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier", "unused")
|
||||
internal suspend fun MessageSource.ensureSequenceIdAvailable() {
|
||||
if (this is OnlineMessageSourceToGroupImpl) {
|
||||
ensureSequenceIdAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("RedundantSuspendModifier", "unused")
|
||||
internal suspend inline fun Message.ensureSequenceIdAvailable() {
|
||||
(this as? MessageChain)?.sourceOrNull?.ensureSequenceIdAvailable()
|
||||
}
|
||||
|
||||
@Serializable(OnlineMessageSourceFromFriendImpl.Serializer::class)
|
||||
internal class OnlineMessageSourceFromFriendImpl(
|
||||
override val bot: Bot,
|
||||
val msg: List<MsgComm.Msg>
|
||||
msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceInternal {
|
||||
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromFriend")
|
||||
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random
|
||||
override val internalIds: IntArray
|
||||
get() = msg.mapToIntArray {
|
||||
it.msgBody.richText.attr?.random ?: 0
|
||||
} // other client 消息的这个是0
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
override val internalIds: IntArray = msg.mapToIntArray {
|
||||
it.msgBody.richText.attr?.random ?: 0
|
||||
} // other client 消息的这个是0
|
||||
override val time: Int = msg.first().msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy {
|
||||
msg.toMessageChain(
|
||||
bot,
|
||||
bot.id,
|
||||
0,
|
||||
null,
|
||||
MessageSourceKind.FRIEND
|
||||
)
|
||||
msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.FRIEND)
|
||||
}
|
||||
override val sender: Friend get() = bot.getFriendOrFail(msg.first().msgHead.fromUin)
|
||||
override val sender: Friend = bot.getFriendOrFail(msg.first().msgHead.fromUin)
|
||||
|
||||
private val jceData by lazy { msg.toJceDataPrivate(internalIds) }
|
||||
|
||||
@ -94,28 +60,21 @@ internal class OnlineMessageSourceFromFriendImpl(
|
||||
@Serializable(OnlineMessageSourceFromStrangerImpl.Serializer::class)
|
||||
internal class OnlineMessageSourceFromStrangerImpl(
|
||||
override val bot: Bot,
|
||||
val msg: List<MsgComm.Msg>
|
||||
msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromStranger(), MessageSourceInternal {
|
||||
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromStranger")
|
||||
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val ids: IntArray get() = sequenceIds// msg.msgBody.richText.attr!!.random
|
||||
override val internalIds: IntArray
|
||||
get() = msg.mapToIntArray {
|
||||
it.msgBody.richText.attr?.random ?: 0
|
||||
} // other client 消息的这个是0
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
override val internalIds: IntArray = msg.mapToIntArray {
|
||||
it.msgBody.richText.attr?.random ?: 0
|
||||
} // other client 消息的这个是0
|
||||
override val time: Int = msg.first().msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy {
|
||||
msg.toMessageChain(
|
||||
bot,
|
||||
bot.id,
|
||||
0,
|
||||
null,
|
||||
MessageSourceKind.STRANGER
|
||||
)
|
||||
msg.toMessageChainNoSource(bot.id, 0, MessageSourceKind.STRANGER)
|
||||
}
|
||||
override val sender: Stranger get() = bot.getStrangerOrFail(msg.first().msgHead.fromUin)
|
||||
override val sender: Stranger = bot.getStrangerOrFail(msg.first().msgHead.fromUin)
|
||||
|
||||
private val jceData by lazy { msg.toJceDataPrivate(internalIds) }
|
||||
|
||||
@ -126,100 +85,101 @@ private fun List<MsgComm.Msg>.toJceDataPrivate(ids: IntArray): ImMsgBody.SourceM
|
||||
val elements = flatMap { it.msgBody.richText.elems }.toMutableList().also {
|
||||
if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
|
||||
}
|
||||
return ImMsgBody.SourceMsg(
|
||||
origSeqs = mapToIntArray { it.msgHead.msgSeq },
|
||||
senderUin = first().msgHead.fromUin,
|
||||
toUin = first().msgHead.toUin,
|
||||
flag = 1,
|
||||
elems = flatMap { it.msgBody.richText.elems },
|
||||
type = 0,
|
||||
time = this.first().msgHead.msgTime,
|
||||
pbReserve = SourceMsg.ResvAttr(
|
||||
origUids = ids.map { it.toLong() and 0xFFFF_FFFF }
|
||||
).toByteArray(SourceMsg.ResvAttr.serializer()),
|
||||
srcMsg = MsgComm.Msg(
|
||||
msgHead = MsgComm.MsgHead(
|
||||
fromUin = this.first().msgHead.fromUin, // qq
|
||||
toUin = this.first().msgHead.toUin, // group
|
||||
msgType = this.first().msgHead.msgType, // 82?
|
||||
c2cCmd = this.first().msgHead.c2cCmd,
|
||||
msgSeq = this.first().msgHead.msgSeq,
|
||||
msgTime = this.first().msgHead.msgTime,
|
||||
msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok
|
||||
// groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode),
|
||||
isSrcMsg = true
|
||||
),
|
||||
msgBody = ImMsgBody.MsgBody(
|
||||
richText = ImMsgBody.RichText(
|
||||
elems = elements
|
||||
|
||||
first().msgHead.run {
|
||||
return ImMsgBody.SourceMsg(
|
||||
origSeqs = mapToIntArray { it.msgHead.msgSeq },
|
||||
senderUin = fromUin,
|
||||
toUin = toUin,
|
||||
flag = 1,
|
||||
elems = flatMap { it.msgBody.richText.elems },
|
||||
type = 0,
|
||||
time = msgTime,
|
||||
pbReserve = SourceMsg.ResvAttr(
|
||||
origUids = ids.map { it.toLong() and 0xFFFF_FFFF }
|
||||
).toByteArray(SourceMsg.ResvAttr.serializer()),
|
||||
srcMsg = MsgComm.Msg(
|
||||
msgHead = MsgComm.MsgHead(
|
||||
fromUin = fromUin, // qq
|
||||
toUin = toUin, // group
|
||||
msgType = msgType, // 82?
|
||||
c2cCmd = c2cCmd,
|
||||
msgSeq = msgSeq,
|
||||
msgTime = msgTime,
|
||||
msgUid = ids.single().toLong() and 0xFFFF_FFFF, // ok
|
||||
// groupInfo = MsgComm.GroupInfo(groupCode = msgHead.groupInfo.groupCode),
|
||||
isSrcMsg = true
|
||||
),
|
||||
msgBody = ImMsgBody.MsgBody(
|
||||
richText = ImMsgBody.RichText(
|
||||
elems = elements
|
||||
)
|
||||
)
|
||||
)
|
||||
).toByteArray(MsgComm.Msg.serializer())
|
||||
)
|
||||
).toByteArray(MsgComm.Msg.serializer())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(OnlineMessageSourceFromTempImpl.Serializer::class)
|
||||
internal class OnlineMessageSourceFromTempImpl(
|
||||
override val bot: Bot,
|
||||
private val msg: List<MsgComm.Msg>
|
||||
msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
|
||||
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromTemp")
|
||||
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||
override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val ids: IntArray get() = sequenceIds//
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
override val time: Int = msg.first().msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy {
|
||||
msg.toMessageChain(
|
||||
bot,
|
||||
bot.id,
|
||||
groupIdOrZero = 0,
|
||||
onlineSource = null,
|
||||
MessageSourceKind.TEMP
|
||||
)
|
||||
msg.toMessageChainNoSource(bot.id, groupIdOrZero = 0, MessageSourceKind.TEMP)
|
||||
}
|
||||
override val sender: Member = with(msg.first().msgHead) {
|
||||
bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin)
|
||||
}
|
||||
override val sender: Member
|
||||
get() = with(msg.first().msgHead) {
|
||||
bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin)
|
||||
}
|
||||
|
||||
private val jceData by lazy { msg.toJceDataPrivate(internalIds) }
|
||||
override fun toJceData(): ImMsgBody.SourceMsg = jceData
|
||||
}
|
||||
|
||||
@Serializable(OnlineMessageSourceFromGroupImpl.Serializer::class)
|
||||
internal data class OnlineMessageSourceFromGroupImpl(
|
||||
internal class OnlineMessageSourceFromGroupImpl(
|
||||
override val bot: Bot,
|
||||
private val msg: List<MsgComm.Msg>
|
||||
msg: List<MsgComm.Msg>
|
||||
) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
|
||||
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceFromGroupImpl")
|
||||
|
||||
@Transient
|
||||
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
|
||||
override val sequenceIds: IntArray get() = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray get() = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||
override val sequenceIds: IntArray = msg.mapToIntArray { it.msgHead.msgSeq }
|
||||
override val internalIds: IntArray = msg.mapToIntArray { it.msgBody.richText.attr!!.random }
|
||||
override val ids: IntArray get() = sequenceIds
|
||||
override val time: Int get() = msg.first().msgHead.msgTime
|
||||
override val time: Int = msg.first().msgHead.msgTime
|
||||
override val originalMessage: MessageChain by lazy {
|
||||
msg.toMessageChain(bot, bot.id, groupIdOrZero = group.id, onlineSource = null, MessageSourceKind.GROUP)
|
||||
msg.toMessageChainNoSource(bot.id, groupIdOrZero = group.id, MessageSourceKind.GROUP)
|
||||
}
|
||||
|
||||
override val sender: Member by lazy {
|
||||
(bot.getGroup(
|
||||
msg.first().msgHead.groupInfo?.groupCode
|
||||
?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
) as GroupImpl).run {
|
||||
get(msg.first().msgHead.fromUin)
|
||||
?: msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }?.run {
|
||||
newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64())
|
||||
}
|
||||
?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
val groupCode = msg.first().msgHead.groupInfo?.groupCode
|
||||
?: error("cannot find groupCode for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
|
||||
val group = bot.getGroup(groupCode)?.checkIsGroupImpl()
|
||||
?: error("cannot find group for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
|
||||
val member = group[msg.first().msgHead.fromUin]
|
||||
if (member != null) return@lazy member
|
||||
|
||||
val anonymousInfo = msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }
|
||||
?: error("cannot find member for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
|
||||
|
||||
anonymousInfo.run {
|
||||
group.newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64())
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJceData(): ImMsgBody.SourceMsg {
|
||||
return ImMsgBody.SourceMsg(
|
||||
private val jceData by lazy {
|
||||
ImMsgBody.SourceMsg(
|
||||
origSeqs = intArrayOf(msg.first().msgHead.msgSeq),
|
||||
senderUin = msg.first().msgHead.fromUin,
|
||||
toUin = 0,
|
||||
@ -231,4 +191,8 @@ internal data class OnlineMessageSourceFromGroupImpl(
|
||||
srcMsg = EMPTY_BYTE_ARRAY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJceData(): ImMsgBody.SourceMsg {
|
||||
return jceData
|
||||
}
|
||||
}
|
292
mirai-core/src/commonMain/kotlin/message/messageToElems.kt
Normal file
292
mirai-core/src/commonMain/kotlin/message/messageToElems.kt
Normal file
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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.internal.message
|
||||
|
||||
import kotlinx.io.core.toByteArray
|
||||
import net.mamoe.mirai.contact.AnonymousMember
|
||||
import net.mamoe.mirai.contact.ContactOrBot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.User
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import net.mamoe.mirai.utils.safeCast
|
||||
import net.mamoe.mirai.utils.zip
|
||||
|
||||
internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510
|
||||
|
||||
|
||||
internal val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
|
||||
internal val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。")
|
||||
internal val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。")
|
||||
internal val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com")
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
internal fun MessageChain.toRichTextElems(
|
||||
messageTarget: ContactOrBot?,
|
||||
withGeneralFlags: Boolean
|
||||
): MutableList<ImMsgBody.Elem> {
|
||||
val forGroup = messageTarget is Group
|
||||
val elements = ArrayList<ImMsgBody.Elem>(this.size)
|
||||
|
||||
if (this.anyIsInstance<QuoteReply>()) {
|
||||
when (val source = this[QuoteReply]!!.source) {
|
||||
is MessageSourceInternal -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
|
||||
else -> error("unsupported MessageSource implementation: ${source::class.simpleName}. Don't implement your own MessageSource.")
|
||||
}
|
||||
}
|
||||
|
||||
var longTextResId: String? = null
|
||||
|
||||
fun transformOneMessage(currentMessage: Message) {
|
||||
if (currentMessage is RichMessage) {
|
||||
val content = currentMessage.content.toByteArray().zip()
|
||||
when (currentMessage) {
|
||||
is ForwardMessageInternal -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = currentMessage.serviceId, // ok
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||
}
|
||||
is LongMessageInternal -> {
|
||||
check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = currentMessage.serviceId, // ok
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||
longTextResId = currentMessage.resId
|
||||
}
|
||||
is LightApp -> elements.add(
|
||||
ImMsgBody.Elem(
|
||||
lightApp = ImMsgBody.LightAppElem(
|
||||
data = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> elements.add(
|
||||
ImMsgBody.Elem(
|
||||
richMsg = ImMsgBody.RichMsg(
|
||||
serviceId = when (currentMessage) {
|
||||
is ServiceMessage -> currentMessage.serviceId
|
||||
else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}")
|
||||
},
|
||||
template1 = byteArrayOf(1) + content
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (currentMessage) {
|
||||
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content)))
|
||||
is CustomMessage -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
customElem = ImMsgBody.CustomElem(
|
||||
enumType = MIRAI_CUSTOM_ELEM_TYPE,
|
||||
data = CustomMessage.dump(
|
||||
currentMessage.getFactory() as CustomMessage.Factory<CustomMessage>,
|
||||
currentMessage
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is At -> {
|
||||
elements.add(ImMsgBody.Elem(text = currentMessage.toJceData(messageTarget.safeCast())))
|
||||
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
|
||||
// removed by https://github.com/mamoe/mirai/issues/524
|
||||
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||
}
|
||||
is PokeMessage -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
commonElem = ImMsgBody.CommonElem(
|
||||
serviceType = 2,
|
||||
businessType = currentMessage.pokeType,
|
||||
pbElem = HummerCommelem.MsgElemInfoServtype2(
|
||||
pokeType = currentMessage.pokeType,
|
||||
vaspokeId = currentMessage.id,
|
||||
vaspokeMinver = "7.2.0",
|
||||
vaspokeName = currentMessage.name
|
||||
).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||
)
|
||||
)
|
||||
)
|
||||
transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN)
|
||||
}
|
||||
|
||||
|
||||
is OfflineGroupImage -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData()))
|
||||
}
|
||||
}
|
||||
is OnlineGroupImageImpl -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate))
|
||||
}
|
||||
}
|
||||
is OnlineFriendImageImpl -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace()))
|
||||
}
|
||||
}
|
||||
is OfflineFriendImage -> {
|
||||
if (messageTarget is User) {
|
||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData()))
|
||||
} else {
|
||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
is FlashImage -> elements.add(currentMessage.toJceData(messageTarget))
|
||||
.also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
|
||||
|
||||
|
||||
is AtAll -> elements.add(atAllData)
|
||||
is Face -> elements.add(
|
||||
if (currentMessage.id >= 260) {
|
||||
ImMsgBody.Elem(commonElem = currentMessage.toCommData())
|
||||
} else {
|
||||
ImMsgBody.Elem(face = currentMessage.toJceData())
|
||||
}
|
||||
)
|
||||
is QuoteReply -> {
|
||||
if (forGroup) {
|
||||
when (val source = currentMessage.source) {
|
||||
is OnlineMessageSource.Incoming.FromGroup -> {
|
||||
val sender0 = source.sender
|
||||
if (sender0 !is AnonymousMember)
|
||||
transformOneMessage(At(sender0))
|
||||
// transformOneMessage(PlainText(" "))
|
||||
// removed by https://github.com/mamoe/mirai/issues/524
|
||||
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is MarketFace -> {
|
||||
if (currentMessage is MarketFaceImpl) {
|
||||
elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate))
|
||||
}
|
||||
//兼容信息
|
||||
transformOneMessage(PlainText(currentMessage.name))
|
||||
if (currentMessage is MarketFaceImpl) {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString()))
|
||||
is PttMessage -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1)
|
||||
)
|
||||
)
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
elemFlags2 = ImMsgBody.ElemFlags2(
|
||||
vipStatus = 1
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is MusicShare -> {
|
||||
// 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
|
||||
// 发送消息时会被特殊处理
|
||||
transformOneMessage(PlainText(currentMessage.content))
|
||||
}
|
||||
|
||||
is ForwardMessage,
|
||||
is MessageSource, // mirai metadata only
|
||||
is RichMessage // already transformed above
|
||||
-> {
|
||||
|
||||
}
|
||||
is InternalFlagOnlyMessage, is ShowImageFlag -> {
|
||||
// ignore
|
||||
}
|
||||
else -> {
|
||||
// unrecognized types are ignored
|
||||
// error("unsupported message type: ${currentMessage::class.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
this.forEach(::transformOneMessage)
|
||||
|
||||
if (withGeneralFlags) {
|
||||
when {
|
||||
longTextResId != null -> {
|
||||
elements.add(
|
||||
ImMsgBody.Elem(
|
||||
generalFlags = ImMsgBody.GeneralFlags(
|
||||
longTextFlag = 1,
|
||||
longTextResid = longTextResId!!,
|
||||
pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
this.anyIsInstance<MarketFaceImpl>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE)))
|
||||
}
|
||||
this.anyIsInstance<RichMessage>() -> {
|
||||
// 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE)))
|
||||
}
|
||||
this.anyIsInstance<FlashImage>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU)))
|
||||
}
|
||||
this.anyIsInstance<PttMessage>() -> {
|
||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT)))
|
||||
}
|
||||
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE)))
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
internal val PB_RESERVE_FOR_RICH_MESSAGE =
|
||||
"08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
|
||||
|
||||
internal val PB_RESERVE_FOR_PTT =
|
||||
"78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes()
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
internal val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes()
|
||||
internal val PB_RESERVE_FOR_MARKET_FACE =
|
||||
"02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes()
|
||||
internal val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
@ -106,40 +106,24 @@ internal class OfflineMessageSourceImplData(
|
||||
}
|
||||
|
||||
internal fun OfflineMessageSourceImplData(
|
||||
bot: Bot?,
|
||||
bot: Bot,
|
||||
delegate: List<MsgComm.Msg>,
|
||||
botId: Long,
|
||||
kind: MessageSourceKind
|
||||
): OfflineMessageSourceImplData {
|
||||
val head = delegate.first().msgHead
|
||||
val kind = when {
|
||||
head.groupInfo != null -> {
|
||||
MessageSourceKind.GROUP
|
||||
}
|
||||
head.c2cTmpMsgHead != null -> {
|
||||
MessageSourceKind.TEMP
|
||||
}
|
||||
bot?.getStranger(head.fromUin) != null -> {
|
||||
MessageSourceKind.STRANGER
|
||||
}
|
||||
else -> {
|
||||
MessageSourceKind.FRIEND
|
||||
}
|
||||
}
|
||||
return OfflineMessageSourceImplData(
|
||||
kind = kind,
|
||||
time = head.msgTime,
|
||||
fromId = head.fromUin,
|
||||
targetId = head.groupInfo?.groupCode ?: head.toUin,
|
||||
originalMessage = delegate.toMessageChain(
|
||||
null,
|
||||
botId,
|
||||
originalMessage = delegate.toMessageChainNoSource(
|
||||
bot.id,
|
||||
groupIdOrZero = head.groupInfo?.groupCode ?: 0,
|
||||
onlineSource = null,
|
||||
messageSourceKind = kind
|
||||
),
|
||||
ids = delegate.mapToIntArray { it.msgHead.msgSeq },
|
||||
internalIds = delegate.mapToIntArray { it.msgHead.msgUid.toInt() },
|
||||
botId = botId
|
||||
botId = bot.id
|
||||
).apply {
|
||||
originElems = delegate.flatMap { it.msgBody.richText.elems }
|
||||
}
|
||||
@ -177,7 +161,7 @@ internal fun OfflineMessageSourceImplData(
|
||||
internalIds = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer())
|
||||
.origUids?.mapToIntArray { it.toInt() } ?: intArrayOf(),
|
||||
time = delegate.time,
|
||||
originalMessageLazy = lazy { delegate.toMessageChain(botId, messageSourceKind, groupIdOrZero) },
|
||||
originalMessageLazy = lazy { delegate.toMessageChainNoSource(botId, messageSourceKind, groupIdOrZero) },
|
||||
fromId = delegate.senderUin,
|
||||
targetId = when {
|
||||
groupIdOrZero != 0L -> groupIdOrZero
|
||||
|
@ -39,6 +39,8 @@ import kotlin.math.roundToInt
|
||||
import kotlin.time.measureTime
|
||||
|
||||
internal object Highway {
|
||||
|
||||
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class BdhUploadResponse(
|
||||
var extendInfo: ByteArray? = null,
|
||||
@ -63,7 +65,7 @@ internal object Highway {
|
||||
if (noBdhAwait) deferred.getCompleted() else deferred.await()
|
||||
}.getOrElse(fallbackSession)
|
||||
|
||||
return tryServers(
|
||||
return tryServersUpload(
|
||||
bot = bot,
|
||||
servers = if (tryOnce) listOf(bdhSession.ssoAddresses.random()) else bdhSession.ssoAddresses,
|
||||
resourceSize = resource.size,
|
||||
@ -121,7 +123,7 @@ internal enum class ChannelKind(
|
||||
override fun toString(): String = display
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified R> tryServers(
|
||||
internal suspend inline fun <reified R> tryServersUpload(
|
||||
bot: QQAndroidBot,
|
||||
servers: Collection<Pair<Int, Int>>,
|
||||
resourceSize: Long,
|
||||
@ -155,6 +157,61 @@ internal suspend inline fun <reified R> tryServers(
|
||||
resp as R
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified R> tryServersDownload(
|
||||
bot: QQAndroidBot,
|
||||
servers: Collection<Pair<Int, Int>>,
|
||||
resourceKind: ResourceKind,
|
||||
channelKind: ChannelKind,
|
||||
crossinline implOnEachServer: suspend (ip: String, port: Int) -> R
|
||||
) = servers.retryWithServers(
|
||||
5000,
|
||||
onFail = { throw IllegalStateException("cannot download $resourceKind, failed on all servers.", it) }
|
||||
) { ip, port ->
|
||||
tryDownloadImplEach(bot, channelKind, resourceKind, ip, port, implOnEachServer)
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified R> tryDownload(
|
||||
bot: QQAndroidBot,
|
||||
host: String,
|
||||
port: Int,
|
||||
times: Int = 1,
|
||||
resourceKind: ResourceKind,
|
||||
channelKind: ChannelKind,
|
||||
crossinline implOnEachServer: suspend (ip: String, port: Int) -> R
|
||||
) = retryCatching(times) {
|
||||
tryDownloadImplEach(bot, channelKind, resourceKind, host, port, implOnEachServer)
|
||||
}.getOrElse { throw IllegalStateException("Cannot download $resourceKind", it) }
|
||||
|
||||
|
||||
private suspend inline fun <reified R> tryDownloadImplEach(
|
||||
bot: QQAndroidBot,
|
||||
channelKind: ChannelKind,
|
||||
resourceKind: ResourceKind,
|
||||
host: String,
|
||||
port: Int,
|
||||
crossinline implOnEachServer: suspend (ip: String, port: Int) -> R
|
||||
): R {
|
||||
bot.network.logger.verbose {
|
||||
"[${channelKind}] Downloading $resourceKind from ${host}:$port"
|
||||
}
|
||||
|
||||
var resp: R? = null
|
||||
runCatching {
|
||||
resp = implOnEachServer(host, port)
|
||||
}.onFailure {
|
||||
bot.network.logger.verbose {
|
||||
"[${channelKind}] Downloading $resourceKind from ${host}:$port failed: $it"
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
bot.network.logger.verbose {
|
||||
"[${channelKind}] Downloading $resourceKind: succeed"
|
||||
}
|
||||
|
||||
return resp as R
|
||||
}
|
||||
|
||||
internal suspend fun ChunkedFlowSession<ByteReadPacket>.sendSequentially(
|
||||
socket: PlatformSocket,
|
||||
respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {}
|
||||
|
@ -16,19 +16,19 @@ import net.mamoe.mirai.internal.utils.io.ProtoBuf
|
||||
|
||||
internal class MsgTransmit : ProtoBuf {
|
||||
@Serializable
|
||||
internal class PbMultiMsgItem(
|
||||
internal class PbMultiMsgItem(
|
||||
@ProtoNumber(1) @JvmField val fileName: String = "",
|
||||
@ProtoNumber(2) @JvmField val buffer: ByteArray = EMPTY_BYTE_ARRAY
|
||||
) : ProtoBuf
|
||||
|
||||
@Serializable
|
||||
internal class PbMultiMsgNew(
|
||||
internal class PbMultiMsgNew(
|
||||
@ProtoNumber(1) @JvmField val msg: List<MsgComm.Msg> = emptyList()
|
||||
) : ProtoBuf
|
||||
|
||||
@Serializable
|
||||
internal class PbMultiMsgTransmit(
|
||||
internal class PbMultiMsgTransmit(
|
||||
@ProtoNumber(1) @JvmField val msg: List<MsgComm.Msg> = emptyList(),
|
||||
@ProtoNumber(2) @JvmField val pbItemList: List<MsgTransmit.PbMultiMsgItem> = emptyList()
|
||||
@ProtoNumber(2) @JvmField val pbItemList: List<PbMultiMsgItem> = emptyList()
|
||||
) : ProtoBuf
|
||||
}
|
@ -23,7 +23,7 @@ internal class MultiMsg : ProtoBuf {
|
||||
|
||||
@Serializable
|
||||
internal class MultiMsgApplyDownReq(
|
||||
@ProtoNumber(1) @JvmField val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(1) @JvmField val msgResid: String = "",
|
||||
@ProtoNumber(2) @JvmField val msgType: Int = 0,
|
||||
@ProtoNumber(3) @JvmField val srcUin: Long = 0L
|
||||
) : ProtoBuf
|
||||
@ -31,11 +31,11 @@ internal class MultiMsg : ProtoBuf {
|
||||
@Serializable
|
||||
internal class MultiMsgApplyDownRsp(
|
||||
@ProtoNumber(1) @JvmField val result: Int = 0,
|
||||
@ProtoNumber(2) @JvmField val thumbDownPara: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(2) @JvmField val thumbDownPara: String = "",
|
||||
@ProtoNumber(3) @JvmField val msgKey: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(4) @JvmField val uint32DownIp: List<Int> = emptyList(),
|
||||
@ProtoNumber(5) @JvmField val uint32DownPort: List<Int> = emptyList(),
|
||||
@ProtoNumber(6) @JvmField val msgResid: ByteArray = EMPTY_BYTE_ARRAY,
|
||||
@ProtoNumber(6) @JvmField val msgResid: String = "",
|
||||
@ProtoNumber(7) @JvmField val msgExternInfo: ExternMsg? = null,
|
||||
@ProtoNumber(8) @JvmField val bytesDownIpV6: List<ByteArray> = emptyList(),
|
||||
@ProtoNumber(9) @JvmField val uint32DownV6Port: List<Int> = emptyList()
|
||||
|
@ -153,6 +153,7 @@ internal object KnownPacketFactories {
|
||||
Heartbeat.Alive,
|
||||
PbMessageSvc.PbMsgWithDraw,
|
||||
MultiMsg.ApplyUp,
|
||||
MultiMsg.ApplyDown,
|
||||
NewContact.SystemMsgNewFriend,
|
||||
NewContact.SystemMsgNewGroup,
|
||||
ProfileService.GroupMngReq,
|
||||
|
@ -14,6 +14,7 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.contact.SendMessageHandler
|
||||
import net.mamoe.mirai.internal.message.contextualBugReportException
|
||||
import net.mamoe.mirai.internal.message.toRichTextElems
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||
@ -151,4 +152,57 @@ internal class MultiMsg {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ApplyDown : OutgoingPacketFactory<ApplyDown.Response>("MultiMsg.ApplyDown") {
|
||||
sealed class Response : Packet {
|
||||
class RequireDownload(
|
||||
val origin: MultiMsg.MultiMsgApplyDownRsp
|
||||
) : Response() {
|
||||
override fun toString(): String = "MultiMsg.ApplyDown.Response"
|
||||
}
|
||||
|
||||
object MessageTooLarge : Response()
|
||||
}
|
||||
|
||||
operator fun invoke(
|
||||
client: QQAndroidClient,
|
||||
buType: Int,
|
||||
resId: String,
|
||||
msgType: Int,
|
||||
) = buildOutgoingUniPacket(client) {
|
||||
writeProtoBuf(
|
||||
MultiMsg.ReqBody.serializer(),
|
||||
MultiMsg.ReqBody(
|
||||
buType = buType, // 1: long, 2: 合并转发
|
||||
buildVer = "8.2.0.1296",
|
||||
multimsgApplydownReq = listOf(
|
||||
MultiMsg.MultiMsgApplyDownReq(
|
||||
msgResid = resId,
|
||||
msgType = msgType,
|
||||
)
|
||||
),
|
||||
netType = 3, // wifi=3, wap=5
|
||||
platformType = 9,
|
||||
subcmd = 2,
|
||||
termType = 5,
|
||||
reqChannelType = 2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
|
||||
val body = readProtoBuf(MultiMsg.RspBody.serializer())
|
||||
val response = body.multimsgApplydownRsp.first()
|
||||
return when (response.result) {
|
||||
0 -> Response.RequireDownload(response)
|
||||
193 -> Response.MessageTooLarge
|
||||
//1 -> Response.OK(resId = response.msgResid)
|
||||
else -> throw contextualBugReportException(
|
||||
"MultiMsg.ApplyDown",
|
||||
response._miraiContentToString(),
|
||||
additional = "Decode failure result=${response.result}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,8 @@ import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.contact.*
|
||||
import net.mamoe.mirai.internal.message.OnlineMessageSourceFromFriendImpl
|
||||
import net.mamoe.mirai.internal.message.toMessageChain
|
||||
import net.mamoe.mirai.internal.message.refine
|
||||
import net.mamoe.mirai.internal.message.toMessageChainOnline
|
||||
import net.mamoe.mirai.internal.network.MultiPacket
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||
@ -49,6 +50,8 @@ import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind.STRANGER
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind.TEMP
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.buildMessageChain
|
||||
import net.mamoe.mirai.utils.*
|
||||
@ -295,7 +298,7 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
|
||||
|
||||
// 有人被邀请(经过同意后)加入 27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 34 30 34 38 32 33 38 35 37 41 37 38 46 33 45 37 35 38 42 39 38 46 43 45 44 43 32 41 30 31 36 36 30 34 31 36 39 35 39 30 38 39 30 39 45 31 34 34
|
||||
// 搜索到群, 直接加入 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35
|
||||
}
|
||||
}
|
||||
|
||||
34 -> { // 与 33 重复
|
||||
return null
|
||||
@ -389,29 +392,22 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
|
||||
friend.checkIsFriendImpl()
|
||||
friend.lastMessageSequence.loop {
|
||||
//我也不知道为什么要这样写,但它就是能跑
|
||||
return if (friend.lastMessageSequence.value != msgHead.msgSeq && friend.lastMessageSequence.compareAndSet(
|
||||
it,
|
||||
msgHead.msgSeq
|
||||
) && contentHead?.autoReply != 1
|
||||
return if (friend.lastMessageSequence.value != msgHead.msgSeq
|
||||
&& friend.lastMessageSequence.compareAndSet(it, msgHead.msgSeq)
|
||||
&& contentHead?.autoReply != 1
|
||||
) {
|
||||
val msgs = friend.friendPkgMsgParsingCache.tryMerge(this)
|
||||
if (msgs.isNotEmpty()) {
|
||||
if (fromSync) {
|
||||
FriendMessageSyncEvent(
|
||||
friend,
|
||||
msgs.toMessageChain(
|
||||
bot = bot, botId = bot.id, groupIdOrZero = 0, onlineSource = true,
|
||||
messageSourceKind = MessageSourceKind.FRIEND
|
||||
),
|
||||
msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND).refine(friend),
|
||||
msgHead.msgTime
|
||||
)
|
||||
} else {
|
||||
FriendMessageEvent(
|
||||
friend,
|
||||
msgs.toMessageChain(
|
||||
bot = bot, botId = bot.id, groupIdOrZero = 0, onlineSource = true,
|
||||
messageSourceKind = MessageSourceKind.FRIEND
|
||||
),
|
||||
msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND).refine(friend),
|
||||
msgHead.msgTime
|
||||
)
|
||||
}
|
||||
@ -430,13 +426,13 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
|
||||
if (fromSync) {
|
||||
StrangerMessageSyncEvent(
|
||||
stranger,
|
||||
toMessageChain(bot, groupIdOrZero = 0, onlineSource = true, MessageSourceKind.STRANGER),
|
||||
listOf(this).toMessageChainOnline(bot, 0, STRANGER).refine(stranger),
|
||||
msgHead.msgTime
|
||||
)
|
||||
} else {
|
||||
StrangerMessageEvent(
|
||||
stranger,
|
||||
toMessageChain(bot, groupIdOrZero = 0, onlineSource = true, MessageSourceKind.STRANGER),
|
||||
listOf(this).toMessageChainOnline(bot, 0, STRANGER).refine(stranger),
|
||||
msgHead.msgTime
|
||||
)
|
||||
}
|
||||
@ -507,26 +503,16 @@ internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean
|
||||
member.lastMessageSequence.loop { instant ->
|
||||
if (msgHead.msgSeq > instant) {
|
||||
if (member.lastMessageSequence.compareAndSet(instant, msgHead.msgSeq)) {
|
||||
if (fromSync) {
|
||||
return GroupTempMessageSyncEvent(
|
||||
return if (fromSync) {
|
||||
GroupTempMessageSyncEvent(
|
||||
member,
|
||||
toMessageChain(
|
||||
bot,
|
||||
groupIdOrZero = 0,
|
||||
onlineSource = true,
|
||||
MessageSourceKind.TEMP
|
||||
),
|
||||
listOf(this).toMessageChainOnline(bot, 0, TEMP).refine(member),
|
||||
msgHead.msgTime
|
||||
)
|
||||
} else {
|
||||
return GroupTempMessageEvent(
|
||||
GroupTempMessageEvent(
|
||||
member,
|
||||
toMessageChain(
|
||||
bot,
|
||||
groupIdOrZero = 0,
|
||||
onlineSource = true,
|
||||
MessageSourceKind.TEMP
|
||||
),
|
||||
listOf(this).toMessageChainOnline(bot, 0, TEMP).refine(member),
|
||||
msgHead.msgTime
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ import net.mamoe.mirai.event.events.GroupMessageSyncEvent
|
||||
import net.mamoe.mirai.event.events.MemberCardChangeEvent
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.contact.*
|
||||
import net.mamoe.mirai.internal.message.toMessageChain
|
||||
import net.mamoe.mirai.internal.message.refine
|
||||
import net.mamoe.mirai.internal.message.toMessageChainOnline
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
|
||||
@ -33,7 +34,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory
|
||||
import net.mamoe.mirai.internal.utils._miraiContentToString
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind.GROUP
|
||||
import net.mamoe.mirai.utils.*
|
||||
|
||||
/**
|
||||
@ -119,12 +120,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
|
||||
|
||||
if (isFromSelfAccount) {
|
||||
return GroupMessageSyncEvent(
|
||||
message = msgs.toMessageChain(
|
||||
bot,
|
||||
groupIdOrZero = group.id,
|
||||
onlineSource = true,
|
||||
MessageSourceKind.GROUP
|
||||
),
|
||||
message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP).refine(group),
|
||||
time = msgHead.msgTime,
|
||||
group = group,
|
||||
sender = sender,
|
||||
@ -137,12 +133,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
|
||||
return GroupMessageEvent(
|
||||
senderName = name,
|
||||
sender = sender,
|
||||
message = msgs.toMessageChain(
|
||||
bot,
|
||||
groupIdOrZero = group.id,
|
||||
onlineSource = true,
|
||||
MessageSourceKind.GROUP
|
||||
),
|
||||
message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP).refine(group),
|
||||
permission = findMemberPermission(extraInfo?.flags ?: 0, sender, bot),
|
||||
time = msgHead.msgTime
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user