From 527fe08446c34e606a7e64ce23c86d838fb8339c Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Thu, 28 Oct 2021 20:43:41 +0800 Subject: [PATCH] Redesign `MessageChain.cleanupRubbishMessageElements()` (#1607) * Redesign `MessageChain.cleanupRubbishMessageElements()` * Fix logic * `CleanupRubbishMessageElementsTest` * Fix testing unit * more testing --- .../kotlin/message/ReceiveMessageHandler.kt | 126 +++++++------- .../CleanupRubbishMessageElementsTest.kt | 156 ++++++++++++++++++ 2 files changed, 222 insertions(+), 60 deletions(-) create mode 100644 mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index 661a98dae..7578b254c 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -218,76 +218,82 @@ internal object ReceiveMessageTransformer { } index++ } + + // delete empty plain text + removeAll { it is PlainText && it.content.isEmpty() } } 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 - } - } + val builder = MessageChainBuilder(initialSize = count()).also { + it.addAll(this) + } - 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 - } + 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) + } + } - 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) + 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 } - - append(element) - - previousLast = last - last = element } - - // 处理分片信息 - compressContinuousPlainText() } + + // 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() } private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) { diff --git a/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt new file mode 100644 index 000000000..14a358075 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt @@ -0,0 +1,156 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message + +import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements +import net.mamoe.mirai.message.data.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.test.assertEquals + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CleanupRubbishMessageElementsTest { + //region + private val replySource = OfflineMessageSourceImplData( + kind = MessageSourceKind.GROUP, + ids = intArrayOf(1), + botId = 1, + time = 1, + fromId = 87, + targetId = 7454, + originalMessage = messageChainOf(), + internalIds = intArrayOf(8711) + ) + + private val source = OfflineMessageSourceImplData( + kind = MessageSourceKind.GROUP, + ids = intArrayOf(1), + botId = 1, + time = 1, + fromId = 2, + targetId = 3, + originalMessage = messageChainOf(), + internalIds = intArrayOf(9) + ) + //endregion + + private fun assertCleanup(excepted: MessageChain, source: MessageChain) { + assertEquals( + excepted, + source.cleanupRubbishMessageElements() + ) + assertEquals( + noMessageSource(excepted), + noMessageSource(source).cleanupRubbishMessageElements() + ) + } + + @Test + fun testQuoteAtSpace() { + // Windows PC QQ + assertCleanup( + messageChainOf(source, QuoteReply(replySource), PlainText("Hello!")), + messageChainOf(source, At(123), PlainText(" "), QuoteReply(replySource), PlainText("Hello!")), + ) + + // QQ Android + assertCleanup( + messageChainOf(source, QuoteReply(replySource), PlainText("Hello!")), + messageChainOf(source, QuoteReply(replySource), At(1234567890), PlainText(" Hello!")), + ) + } + + @Test + fun testTIMAudio() { + val audio = OnlineAudioImpl("0", byteArrayOf(), 0, AudioCodec.SILK, "", 0, null) + assertCleanup( + messageChainOf(source, audio), + messageChainOf(source, audio, UNSUPPORTED_VOICE_MESSAGE_PLAIN), + ) + } + + @Test + fun testPokeMessageCleanup() { + val poke = PokeMessage("", 1, 1) + assertCleanup( + messageChainOf(source, poke), + messageChainOf(source, poke, UNSUPPORTED_POKE_MESSAGE_PLAIN), + ) + } + + @Test + fun testVipFaceCleanup() { + val vf = VipFace(VipFace.Kind(1, "Test!"), 50) + assertCleanup( + messageChainOf(source, vf), + messageChainOf(source, vf, PlainText("----CCCCCTest!")), + ) + } + + @Test + fun testLongMessageInternalCleanup() { + val li = LongMessageInternal("", "") + assertCleanup( + messageChainOf(source, li), + messageChainOf(source, li, UNSUPPORTED_MERGED_MESSAGE_PLAIN), + ) + } + + @Test + fun testCompressContinuousPlainText() { + assertCleanup( + messageChainOf(PlainText("1234567890")), + "12 3 45 6 789 0".split(" ").map(::PlainText).toMessageChain(), + ) + assertCleanup( + msg(source, At(123456), "Hello! How are you?"), + msg(source, At(123456), "Hello", "!", " ", "How", " ", "are ", "you?"), + ) + } + + @Test + fun testEmptyPlainTextRemoved() { + assertCleanup( + messageChainOf(), + " ".split(" ").map(::PlainText).toMessageChain(), + ) + assertCleanup( + msg(AtAll), + msg("", AtAll, "", "", ""), + ) + } + + @Test + fun testBlankPlainTextLiving() { + assertCleanup( + msg(" "), + msg("", " ", " ", " "), + ) + } + + //region + + private fun msg(vararg msgs: Any?): MessageChain { + return msgs.map { elm -> + when (elm) { + is Message -> elm + is String -> PlainText(elm) + else -> PlainText(elm.toString()) + } + }.toMessageChain() + } + + private fun noMessageSource(c: MessageChain): MessageChain { + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + return createMessageChainImplOptimized(c.filterNot { it is MessageSource }) + } + + //endregion +} \ No newline at end of file