From 1a8172437ba894773f9ca12dd366739608bf4bf7 Mon Sep 17 00:00:00 2001
From: Karlatemp <karlatemp@vip.qq.com>
Date: Sat, 16 Oct 2021 16:00:31 +0800
Subject: [PATCH 1/5] Redesign `MessageChain.cleanupRubbishMessageElements()`

---
 .../kotlin/message/ReceiveMessageHandler.kt   | 120 +++++++++---------
 1 file changed, 63 insertions(+), 57 deletions(-)

diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt
index 661a98dae44..20d3eac3cb6 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)
+        // TIM audios
+        if (builder.any { it is Audio }) {
+            builder.remove(UNSUPPORTED_VOICE_MESSAGE_PLAIN)
+        }
 
-                previousLast = last
-                last = element
+        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)
+                    }
+                }
             }
+        }
 
-            // 处理分片信息
-            compressContinuousPlainText()
+        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) {

From 90b5e3156cd16d0257a3debfca72f1e81eed014a Mon Sep 17 00:00:00 2001
From: Karlatemp <karlatemp@vip.qq.com>
Date: Sat, 16 Oct 2021 18:53:23 +0800
Subject: [PATCH 2/5] Fix logic

---
 .../src/commonMain/kotlin/message/ReceiveMessageHandler.kt  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt
index 20d3eac3cb6..7578b254c6c 100644
--- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt
+++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt
@@ -241,7 +241,7 @@ internal object ReceiveMessageTransformer {
 
         kotlin.run quote@{
             val quoteReplyIndex = builder.indexOfFirst { it is QuoteReply }
-            if (quoteReplyIndex > 0) {
+            if (quoteReplyIndex >= 0) {
                 // QuoteReply + At + PlainText(space 1)
                 if (quoteReplyIndex < builder.size - 1) {
                     if (builder[quoteReplyIndex + 1] is At) {
@@ -269,7 +269,7 @@ internal object ReceiveMessageTransformer {
 
         kotlin.run { // VipFace
             val vipFaceIndex = builder.indexOfFirst { it is VipFace }
-            if (vipFaceIndex > 0 && vipFaceIndex < builder.size - 1) {
+            if (vipFaceIndex >= 0 && vipFaceIndex < builder.size - 1) {
                 val l = builder[vipFaceIndex] as VipFace
                 val text = builder[vipFaceIndex + 1]
                 if (text is PlainText) {
@@ -281,7 +281,7 @@ internal object ReceiveMessageTransformer {
         }
 
         fun removeSuffixText(index: Int, text: PlainText) {
-            if (index > 0 && index < builder.size - 1) {
+            if (index >= 0 && index < builder.size - 1) {
                 if (builder[index + 1] == text) {
                     builder.removeAt(index + 1)
                 }

From fdac8a638cdecf49ba574011429cf9e969c45c17 Mon Sep 17 00:00:00 2001
From: Karlatemp <karlatemp@vip.qq.com>
Date: Sat, 16 Oct 2021 18:54:07 +0800
Subject: [PATCH 3/5] `CleanupRubbishMessageElementsTest`

---
 .../CleanupRubbishMessageElementsTest.kt      | 126 ++++++++++++++++++
 1 file changed, 126 insertions(+)
 create mode 100644 mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt

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 00000000000..abfa0fb7087
--- /dev/null
+++ b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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 noMessageSource(c: MessageChain): MessageChain {
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        return createMessageChainImplOptimized(c.filterNot { it is MessageSource })
+    }
+
+    private fun assertCleanup(excepted: MessageChain, source: MessageChain) {
+        assertEquals(
+            excepted,
+            source.cleanupRubbishMessageElements()
+        )
+        assertEquals(
+            noMessageSource(excepted),
+            noMessageSource(source).cleanupRubbishMessageElements()
+        )
+    }
+
+    @Test
+    fun testCleanupNoSource() {
+        // Windows PC QQ
+        assertCleanup(
+            messageChainOf(QuoteReply(replySource), PlainText("Hello!")),
+            messageChainOf(At(123), PlainText(" "), QuoteReply(replySource), PlainText("Hello!")),
+        )
+
+        // QQ Android
+        assertCleanup(
+            messageChainOf(QuoteReply(replySource), PlainText("Hello!")),
+            messageChainOf(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(),
+        )
+    }
+
+    @Test
+    fun testEmptyPlainTextRemoved() {
+        assertCleanup(
+            messageChainOf(),
+            "                     ".split(" ").map(::PlainText).toMessageChain(),
+        )
+    }
+}
\ No newline at end of file

From 14309dec74dedfe7044915ed8783f6907b98c5a2 Mon Sep 17 00:00:00 2001
From: Karlatemp <karlatemp@vip.qq.com>
Date: Sat, 16 Oct 2021 18:58:11 +0800
Subject: [PATCH 4/5] Fix testing unit

---
 .../message/CleanupRubbishMessageElementsTest.kt       | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
index abfa0fb7087..cf0c2ffb606 100644
--- a/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
@@ -58,17 +58,17 @@ internal class CleanupRubbishMessageElementsTest {
     }
 
     @Test
-    fun testCleanupNoSource() {
+    fun testQuoteAtSpace() {
         // Windows PC QQ
         assertCleanup(
-            messageChainOf(QuoteReply(replySource), PlainText("Hello!")),
-            messageChainOf(At(123), PlainText(" "), QuoteReply(replySource), PlainText("Hello!")),
+            messageChainOf(source, QuoteReply(replySource), PlainText("Hello!")),
+            messageChainOf(source, At(123), PlainText(" "), QuoteReply(replySource), PlainText("Hello!")),
         )
 
         // QQ Android
         assertCleanup(
-            messageChainOf(QuoteReply(replySource), PlainText("Hello!")),
-            messageChainOf(QuoteReply(replySource), At(1234567890), PlainText(" Hello!")),
+            messageChainOf(source, QuoteReply(replySource), PlainText("Hello!")),
+            messageChainOf(source, QuoteReply(replySource), At(1234567890), PlainText(" Hello!")),
         )
     }
 

From 7c24d022c83d596488374cac48595b29eca544fa Mon Sep 17 00:00:00 2001
From: Karlatemp <karlatemp@vip.qq.com>
Date: Sun, 17 Oct 2021 12:49:54 +0800
Subject: [PATCH 5/5] more testing

---
 .../CleanupRubbishMessageElementsTest.kt      | 40 ++++++++++++++++---
 1 file changed, 35 insertions(+), 5 deletions(-)

diff --git a/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
index cf0c2ffb606..14a358075f7 100644
--- a/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/CleanupRubbishMessageElementsTest.kt
@@ -41,11 +41,6 @@ internal class CleanupRubbishMessageElementsTest {
     )
     //endregion
 
-    private fun noMessageSource(c: MessageChain): MessageChain {
-        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-        return createMessageChainImplOptimized(c.filterNot { it is MessageSource })
-    }
-
     private fun assertCleanup(excepted: MessageChain, source: MessageChain) {
         assertEquals(
             excepted,
@@ -114,6 +109,10 @@ internal class CleanupRubbishMessageElementsTest {
             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
@@ -122,5 +121,36 @@ internal class CleanupRubbishMessageElementsTest {
             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