/* * 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 io.ktor.utils.io.core.* 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.toAudio import net.mamoe.mirai.internal.message.data.LongMessageInternal import net.mamoe.mirai.internal.message.data.OnlineAudioImpl import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade import net.mamoe.mirai.internal.message.protocol.impl.PokeMessageProtocol.Companion.UNSUPPORTED_POKE_MESSAGE_PLAIN import net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol.Companion.UNSUPPORTED_MERGED_MESSAGE_PLAIN import net.mamoe.mirai.internal.message.source.* import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.structureToString import net.mamoe.mirai.utils.toLongUnsigned /** * 只在手动构造 [OfflineMessageSource] 时调用 */ internal fun ImMsgBody.SourceMsg.toMessageChainNoSource( bot: Bot, messageSourceKind: MessageSourceKind, groupIdOrZero: Long, refineContext: RefineContext = EmptyRefineContext, facade: MessageProtocolFacade = MessageProtocolFacade ): MessageChain { val elements = this.elems return buildMessageChain(elements.size + 1) { facade.decode(elements, groupIdOrZero, messageSourceKind, bot, this, null) }.cleanupRubbishMessageElements().refineLight(bot, refineContext) } internal suspend fun List.toMessageChainOnline( bot: Bot, groupIdOrZero: Long, messageSourceKind: MessageSourceKind, refineContext: RefineContext = EmptyRefineContext, facade: MessageProtocolFacade = MessageProtocolFacade ): MessageChain { return toMessageChain(bot, groupIdOrZero, true, messageSourceKind, facade).refineDeep(bot, refineContext) } internal fun getMessageSourceKindFromC2cCmdOrNull(c2cCmd: Int): MessageSourceKind? { return when (c2cCmd) { 11 -> MessageSourceKind.FRIEND // bot 给其他人发消息 4 -> MessageSourceKind.FRIEND // bot 给自己作为好友发消息 (非 other client) 1 -> MessageSourceKind.GROUP else -> null } } internal fun getMessageSourceKindFromC2cCmd(c2cCmd: Int): MessageSourceKind { return getMessageSourceKindFromC2cCmdOrNull(c2cCmd) ?: error("Could not get source kind from c2cCmd: $c2cCmd") } internal suspend fun MsgComm.Msg.toMessageChainOnline( bot: Bot, refineContext: RefineContext = EmptyRefineContext, facade: MessageProtocolFacade = MessageProtocolFacade, ): MessageChain { val kind = getMessageSourceKindFromC2cCmd(msgHead.c2cCmd) val groupId = when (kind) { MessageSourceKind.GROUP -> msgHead.groupInfo?.groupCode ?: 0 else -> 0 } return listOf(this).toMessageChainOnline(bot, groupId, kind, refineContext, facade) } //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, facade: MessageProtocolFacade = MessageProtocolFacade, ): MessageChain { try { return toMessageChainImpl(bot, groupIdOrZero, onlineSource, messageSourceKind, facade) } catch (e: Exception) { throw IllegalStateException( "Failed to transform internal message to facade message, msg=${this@toMessageChain.structureToString()}", e ) } } private fun List.toMessageChainImpl( bot: Bot, groupIdOrZero: Long, onlineSource: Boolean?, messageSourceKind: MessageSourceKind, facade: MessageProtocolFacade = MessageProtocolFacade, ): MessageChain { val messageList = this val builder = MessageChainBuilder(messageList.sumOf { it.msgBody.richText.elems.size }) if (onlineSource != null) { builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList)) } messageList.forEach { msg -> facade.decode(msg.msgBody.richText.elems, groupIdOrZero, messageSourceKind, bot, builder, msg) } 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 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++ } // delete empty plain text removeAll { it is PlainText && it.content.isEmpty() } } fun MessageChain.cleanupRubbishMessageElements(): MessageChain { val builder = MessageChainBuilder(initialSize = count()).also { it.addAll(this) } kotlin.run moveQuoteReply@{ // Move QuoteReply after MessageSource val exceptedQuoteReplyIndex = builder.indexOfFirst { it is MessageSource } + 1 val quoteReplyIndex = builder.indexOfFirst { it is QuoteReply } if (quoteReplyIndex < 1) return@moveQuoteReply if (quoteReplyIndex != exceptedQuoteReplyIndex) { val qr = builder[quoteReplyIndex] builder.removeAt(quoteReplyIndex) builder.add(exceptedQuoteReplyIndex, qr) } } kotlin.run quote@{ val quoteReplyIndex = builder.indexOfFirst { it is QuoteReply } if (quoteReplyIndex >= 0) { // QuoteReply + At + PlainText(space 1) if (quoteReplyIndex < builder.size - 1) { if (builder[quoteReplyIndex + 1] is At) { builder.removeAt(quoteReplyIndex + 1) } if (quoteReplyIndex < builder.size - 1) { val elm = builder[quoteReplyIndex + 1] if (elm is PlainText && elm.content.startsWith(' ')) { if (elm.content.length == 1) { builder.removeAt(quoteReplyIndex + 1) } else { builder[quoteReplyIndex + 1] = PlainText(elm.content.substring(1)) } } } return@quote } } } // TIM audios if (builder.any { it is Audio }) { builder.remove(UNSUPPORTED_VOICE_MESSAGE_PLAIN) } kotlin.run { // VipFace val vipFaceIndex = builder.indexOfFirst { it is VipFace } if (vipFaceIndex >= 0 && vipFaceIndex < builder.size - 1) { val l = builder[vipFaceIndex] as VipFace val text = builder[vipFaceIndex + 1] if (text is PlainText) { if (text.content.length == 4 + (l.count / 10) + l.kind.name.length) { builder.removeAt(vipFaceIndex + 1) } } } } fun removeSuffixText(index: Int, text: PlainText) { if (index >= 0 && index < builder.size - 1) { if (builder[index + 1] == text) { builder.removeAt(index + 1) } } } removeSuffixText(builder.indexOfFirst { it is LongMessageInternal }, UNSUPPORTED_MERGED_MESSAGE_PLAIN) removeSuffixText(builder.indexOfFirst { it is PokeMessage }, UNSUPPORTED_POKE_MESSAGE_PLAIN) builder.compressContinuousPlainText() return builder.asMessageChain() } fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl( filename = fileName.decodeToString(), fileMd5 = fileMd5, fileSize = fileSize.toLongUnsigned(), codec = AudioCodec.fromId(format), url = downPara.decodeToString(), length = time.toLongUnsigned(), originalPtt = this, ) }