123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- /*
- * 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<MsgComm.Msg>.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<MsgComm.Msg>.toMessageChainOffline(
- // bot: Bot,
- // groupIdOrZero: Long,
- // messageSourceKind: MessageSourceKind
- //): MessageChain {
- // return toMessageChain(bot, groupIdOrZero, false, messageSourceKind).refineLight(bot)
- //}
- internal fun List<MsgComm.Msg>.toMessageChainNoSource(
- bot: Bot,
- groupIdOrZero: Long,
- messageSourceKind: MessageSourceKind,
- refineContext: RefineContext = EmptyRefineContext,
- ): MessageChain {
- return toMessageChain(bot, groupIdOrZero, null, messageSourceKind).refineLight(bot, refineContext)
- }
- private fun List<MsgComm.Msg>.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<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,
- 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群
- /*
- <?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()) {
- // 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,
- )
- }
|