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:
Him188 2021-02-03 09:37:33 +08:00 committed by GitHub
commit 66ef872962
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1374 additions and 885 deletions

View File

@ -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

View File

@ -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>
/**
* 通过好友验证

View File

@ -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
}

View File

@ -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

View File

@ -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")
}
}
}
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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,

View File

@ -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?
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View 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()

View File

@ -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

View File

@ -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 = {}

View File

@ -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
}

View File

@ -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()

View File

@ -153,6 +153,7 @@ internal object KnownPacketFactories {
Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw,
MultiMsg.ApplyUp,
MultiMsg.ApplyDown,
NewContact.SystemMsgNewFriend,
NewContact.SystemMsgNewGroup,
ProfileService.GroupMngReq,

View File

@ -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}"
)
}
}
}
}

View File

@ -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
)
}

View File

@ -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
)