/* * 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 kotlinx.io.core.readUShort import kotlinx.serialization.json.Json import net.mamoe.mirai.Bot import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio import net.mamoe.mirai.internal.network.protocol.data.proto.* import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* /** * 只在手动构造 [OfflineMessageSource] 时调用 */ internal fun ImMsgBody.SourceMsg.toMessageChainNoSource( bot: Bot, messageSourceKind: MessageSourceKind, groupIdOrZero: Long, refineContext: RefineContext = EmptyRefineContext, ): MessageChain { val elements = this.elems return buildMessageChain(elements.size + 1) { joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, this) }.cleanupRubbishMessageElements().refineLight(bot, refineContext) } internal suspend fun List.toMessageChainOnline( bot: Bot, groupIdOrZero: Long, messageSourceKind: MessageSourceKind, refineContext: RefineContext = EmptyRefineContext, ): MessageChain { return toMessageChain(bot, groupIdOrZero, true, messageSourceKind).refineDeep(bot, refineContext) } internal suspend fun MsgComm.Msg.toMessageChainOnline( bot: Bot, refineContext: RefineContext = EmptyRefineContext, ): MessageChain { fun getSourceKind(c2cCmd: Int): MessageSourceKind { return when (c2cCmd) { 11 -> MessageSourceKind.FRIEND // bot 给其他人发消息 4 -> MessageSourceKind.FRIEND // bot 给自己作为好友发消息 (非 other client) 1 -> MessageSourceKind.GROUP else -> error("Could not get source kind from c2cCmd: $c2cCmd") } } val kind = getSourceKind(msgHead.c2cCmd) val groupId = when (kind) { MessageSourceKind.GROUP -> msgHead.groupInfo?.groupCode ?: 0 else -> 0 } return listOf(this).toMessageChainOnline(bot, groupId, kind, refineContext) } //internal fun List.toMessageChainOffline( // bot: Bot, // groupIdOrZero: Long, // messageSourceKind: MessageSourceKind //): MessageChain { // return toMessageChain(bot, groupIdOrZero, false, messageSourceKind).refineLight(bot) //} internal fun List.toMessageChainNoSource( bot: Bot, groupIdOrZero: Long, messageSourceKind: MessageSourceKind, refineContext: RefineContext = EmptyRefineContext, ): MessageChain { return toMessageChain(bot, groupIdOrZero, null, messageSourceKind).refineLight(bot, refineContext) } private fun List.toMessageChain( bot: Bot, 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) { builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList)) } joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder) for (msg in messageList) { msg.msgBody.richText.ptt?.toAudio()?.let { builder.add(it) } } return builder.build().cleanupRubbishMessageElements() } /** * 接收消息的解析器. 将 [MsgComm.Msg] 转换为对应的 [SingleMessage] * @see joinToMessageChain */ internal object ReceiveMessageTransformer { fun createMessageSource( bot: Bot, onlineSource: Boolean, messageSourceKind: MessageSourceKind, messageList: List, ): 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, groupIdOrZero: Long, messageSourceKind: MessageSourceKind, bot: Bot, builder: MessageChainBuilder, ) { // ProtoBuf.encodeToHexString(elements).soutv("join") // (this._miraiContentToString().soutv()) for (element in elements) { transformElement(element, groupIdOrZero, messageSourceKind, bot, builder) when { element.richMsg != null -> decodeRichMessage(element.richMsg, builder) } } } private fun transformElement( element: ImMsgBody.Elem, groupIdOrZero: Long, messageSourceKind: MessageSourceKind, bot: Bot, builder: MessageChainBuilder, ) { when { element.srcMsg != null -> decodeSrcMsg(element.srcMsg, builder, bot, 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(MarketFaceInternal(element.marketFace)) element.lightApp != null -> decodeLightApp(element.lightApp, builder) element.customElem != null -> decodeCustomElem(element.customElem, builder) element.commonElem != null -> decodeCommonElem(element.commonElem, builder) element.transElemInfo != null -> decodeTransElem(element.transElemInfo, builder) element.elemFlags2 != null || element.extraInfo != null || element.generalFlags != null || element.anonGroupMsg != null -> { // ignore } else -> { UnsupportedMessageImpl(element).takeIf { it.struct.isNotEmpty() }?.let(builder::add) // println(it._miraiContentToString()) } } } fun MessageChainBuilder.compressContinuousPlainText() { var index = 0 val builder = StringBuilder() while (index + 1 < size) { val elm0 = get(index) val elm1 = get(index + 1) if (elm0 is PlainText && elm1 is PlainText) { builder.setLength(0) var end = -1 for (i in index until size) { val elm = get(i) if (elm is PlainText) { end = i builder.append(elm.content) } else break } set(index, PlainText(builder.toString())) // do delete val index1 = index + 1 repeat(end - index) { removeAt(index1) } } index++ } } 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) } } } append(element) previousLast = last last = element } // 处理分片信息 compressContinuousPlainText() } } 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, bot: Bot, messageSourceKind: MessageSourceKind, groupIdOrZero: Long, ) { list.add(QuoteReply(OfflineMessageSourceImplData(srcMsg, bot, 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(LightAppInternal(content)) } 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 decodeTransElem( transElement: ImMsgBody.TransElem, list: MessageChainBuilder, ) { // file // type=24 when (transElement.elemType) { 24 -> transElement.elemValue.read { // group file feed // 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63 // fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? { // if (file.ext.isEmpty()) return null // val element = kotlin.runCatching { // jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject // }.getOrNull() ?: return null // val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null // return extInfo.loadAs(HummerResv21.ResvAttr.serializer()) // } val var7 = readByte() if (var7 == 1.toByte()) { while (remaining > 2) { val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readUShort().toInt()) // proto.msgType=6 val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only. // val attr = getFileRsrvAttr(file) ?: continue // val info = attr.forwardExtFileInfo ?: continue list.add( FileMessageImpl( id = file.filePath, busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a name = file.fileName, size = file.fileSize ) ) } } } } } private val jsonForFileDecode = Json { isLenient = true coerceInputValues = true } 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群 /* */ /** * 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()) { // Nested ForwardMessage val fileName = findStringProperty("m_fileName") if (fileName.isNotEmpty() && findStringProperty("action") == "viewMultiMsg") { ForwardMessageInternal(content, null, fileName) } else { SimpleServiceMessage(35, content) } } else when (findStringProperty("multiMsgFlag").toIntOrNull()) { 1 -> LongMessageInternal(content, resId) 0 -> ForwardMessageInternal(content, resId, null) else -> { // from PC QQ if (findStringProperty("action") == "viewMultiMsg") { ForwardMessageInternal(content, resId, null) } else { SimpleServiceMessage(35, content) } } } builder.add(msg) } // 104 新群员入群的消息 else -> { builder.add(SimpleServiceMessage(richMsg.serviceId, content)) } } } fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl( filename = fileName.encodeToString(), fileMd5 = fileMd5, fileSize = fileSize.toLongUnsigned(), codec = AudioCodec.fromId(format), url = downPara.encodeToString(), length = time.toLongUnsigned(), originalPtt = this, ) }