From 2d0b4d470a837da611437f1152c5cb4c46d00349 Mon Sep 17 00:00:00 2001
From: StageGuard <1355416608@qq.com>
Date: Tue, 8 Nov 2022 09:33:42 +0800
Subject: [PATCH] [core] Proposal implementation of `RoamingSupported` for
 `Group`

---
 .../src/commonMain/kotlin/contact/Group.kt    |  3 +-
 .../src/internal/contact/MockGroupImpl.kt     |  4 ++
 mirai-core-mock/test/mock/MessagingTest.kt    | 23 ++++--
 .../commonMain/kotlin/contact/GroupImpl.kt    |  4 ++
 .../contact/roaming/RoamingMessagesImpl.kt    | 71 +++++++++++++++++++
 .../src/commonMain/kotlin/utils/collection.kt | 28 ++++++++
 6 files changed, 128 insertions(+), 5 deletions(-)
 create mode 100644 mirai-core/src/commonMain/kotlin/utils/collection.kt

diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
index 76436b58d..069f6ae2a 100644
--- a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
+++ b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt
@@ -18,6 +18,7 @@ import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.active.GroupActive
 import net.mamoe.mirai.contact.announcement.Announcements
 import net.mamoe.mirai.contact.file.RemoteFiles
+import net.mamoe.mirai.contact.roaming.RoamingSupported
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
@@ -60,7 +61,7 @@ import kotlin.jvm.JvmSynthetic
  * 使用 [Group.files] 获取群文件管理器后操作. 详见 [RemoteFiles].
  */
 @NotStableForInheritance
-public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported {
+public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported, RoamingSupported {
     /**
      * 群名称.
      *
diff --git a/mirai-core-mock/src/internal/contact/MockGroupImpl.kt b/mirai-core-mock/src/internal/contact/MockGroupImpl.kt
index ad78778bd..257670ee0 100644
--- a/mirai-core-mock/src/internal/contact/MockGroupImpl.kt
+++ b/mirai-core-mock/src/internal/contact/MockGroupImpl.kt
@@ -17,6 +17,7 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.contact.announcement.OfflineAnnouncement
 import net.mamoe.mirai.contact.announcement.buildAnnouncementParameters
 import net.mamoe.mirai.contact.file.RemoteFiles
+import net.mamoe.mirai.contact.roaming.RoamingMessages
 import net.mamoe.mirai.data.GroupHonorType
 import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.broadcast
@@ -31,6 +32,7 @@ import net.mamoe.mirai.mock.contact.MockGroupControlPane
 import net.mamoe.mirai.mock.contact.MockNormalMember
 import net.mamoe.mirai.mock.contact.active.MockGroupActive
 import net.mamoe.mirai.mock.internal.contact.active.MockGroupActiveImpl
+import net.mamoe.mirai.mock.internal.contact.roaming.MockRoamingMessages
 import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToGroup
 import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
 import net.mamoe.mirai.mock.utils.broadcastBlocking
@@ -353,4 +355,6 @@ internal class MockGroupImpl(
     override fun toString(): String {
         return "Group($id)"
     }
+
+    override val roamingMessages: RoamingMessages = MockRoamingMessages(this)
 }
\ No newline at end of file
diff --git a/mirai-core-mock/test/mock/MessagingTest.kt b/mirai-core-mock/test/mock/MessagingTest.kt
index 8d341bc8c..8ffef3c08 100644
--- a/mirai-core-mock/test/mock/MessagingTest.kt
+++ b/mirai-core-mock/test/mock/MessagingTest.kt
@@ -12,11 +12,8 @@ package net.mamoe.mirai.mock.test.mock
 import kotlinx.coroutines.flow.toList
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.MessageSource.Key.recall
-import net.mamoe.mirai.message.data.OnlineMessageSource
-import net.mamoe.mirai.message.data.PlainText
-import net.mamoe.mirai.message.data.messageChainOf
-import net.mamoe.mirai.message.data.source
 import net.mamoe.mirai.mock.MockActions.mockFireRecalled
 import net.mamoe.mirai.mock.test.MockBotTestBase
 import net.mamoe.mirai.mock.utils.broadcastMockEvents
@@ -148,6 +145,24 @@ internal class MessagingTest: MockBotTestBase() {
             assertEquals(messageChainOf(PlainText("Test2!")), messages[1])
             assertEquals(messageChainOf(PlainText("Pong!")), messages[2])
         }
+
+        val mockGroup = bot.addGroup(2, "2")
+        val mockGroupMember1 = mockGroup.addMember(123, "123")
+        val mockGroupMember2 = mockGroup.addMember(124, "124")
+        val mockGroupMember3 = mockGroup.addMember(125, "125")
+
+        broadcastMockEvents {
+            mockGroupMember1 says { append("msg1") }
+            mockGroupMember2 says { append("msg2") }
+            mockGroupMember3 says { append("msg3") }
+        }
+
+        with(mockGroup.roamingMessages.getAllMessages().toList()) {
+            assertEquals(3, size)
+            assertEquals(messageChainOf(PlainText("msg1")), get(0))
+            assertEquals(messageChainOf(PlainText("msg2")), get(1))
+            assertEquals(messageChainOf(PlainText("msg3")), get(2))
+        }
     }
 
     @Test
diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
index 8f312ae48..7b879c7dc 100644
--- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
@@ -20,6 +20,7 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.contact.active.GroupActive
 import net.mamoe.mirai.contact.announcement.Announcements
 import net.mamoe.mirai.contact.file.RemoteFiles
+import net.mamoe.mirai.contact.roaming.RoamingMessages
 import net.mamoe.mirai.data.GroupHonorType
 import net.mamoe.mirai.data.GroupInfo
 import net.mamoe.mirai.data.MemberInfo
@@ -30,6 +31,7 @@ import net.mamoe.mirai.internal.contact.active.GroupActiveImpl
 import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
 import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl
 import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
+import net.mamoe.mirai.internal.contact.roaming.RoamingMessagesImplGroup
 import net.mamoe.mirai.internal.message.contextualBugReportException
 import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
 import net.mamoe.mirai.internal.message.image.OfflineGroupImage
@@ -389,6 +391,8 @@ internal abstract class CommonGroupImpl constructor(
         return result.success
     }
 
+    override val roamingMessages: RoamingMessages by lazy { RoamingMessagesImplGroup(this) }
+
     override fun toString(): String = "Group($id)"
 }
 
diff --git a/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImpl.kt b/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImpl.kt
index 15e180c08..6db85639c 100644
--- a/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImpl.kt
@@ -20,10 +20,14 @@ import net.mamoe.mirai.contact.roaming.RoamingMessage
 import net.mamoe.mirai.contact.roaming.RoamingMessageFilter
 import net.mamoe.mirai.contact.roaming.RoamingMessages
 import net.mamoe.mirai.internal.contact.AbstractContact
+import net.mamoe.mirai.internal.contact.CommonGroupImpl
 import net.mamoe.mirai.internal.contact.FriendImpl
 import net.mamoe.mirai.internal.message.toMessageChainOnline
 import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
+import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetGroupMsg
 import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetRoamMsgReq
+import net.mamoe.mirai.internal.utils.indexFirstBE
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.utils.check
 import net.mamoe.mirai.utils.mapToIntArray
@@ -106,4 +110,71 @@ internal class RoamingMessagesImplFriend(
             )
         ).value.check()
     }
+}
+
+internal class RoamingMessagesImplGroup(
+    override val contact: CommonGroupImpl
+) : RoamingMessagesImpl() {
+    override suspend fun requestRoamMsg(
+        timeStart: Long,
+        lastMessageTime: Long,
+        random: Long // unused field
+    ): MessageSvcPbGetRoamMsgReq.Response {
+        val lastMsgSeq = contact.bot.network.sendAndExpect(
+            TroopManagement.GetGroupLastMsgSeq(
+                client = contact.bot.client,
+                groupUin = contact.uin
+            )
+        )
+        return when(lastMsgSeq) {
+            is TroopManagement.GetGroupLastMsgSeq.Response.Success -> {
+                val results = mutableListOf<MsgComm.Msg>()
+                var currentSeq = lastMsgSeq.seq
+
+                while (true) {
+                    if (currentSeq <= 0) break
+
+                    val resp = contact.bot.network.sendAndExpect(
+                        MessageSvcPbGetGroupMsg(
+                            client = contact.bot.client,
+                            groupUin = contact.uin,
+                            messageSequence = currentSeq,
+                            20 // maximum 20
+                        )
+                    )
+                    if (resp is MessageSvcPbGetGroupMsg.Failed) break
+                    if ((resp as MessageSvcPbGetGroupMsg.Success).msgElem.isEmpty()) break
+
+                    // the message may be sorted increasing by message time,
+                    // if so, additional sortBy will not take cost.
+                    val msgElems = resp.msgElem.sortedBy { it.msgHead.msgTime }
+                    results.addAll(0, msgElems)
+
+                    val firstMsgElem = msgElems.first()
+                    if (firstMsgElem.msgHead.msgTime < timeStart) {
+                        break
+                    } else {
+                        currentSeq = (firstMsgElem.msgHead.msgSeq - 1).toLong()
+                    }
+                }
+
+                // use binary search to find the first message that message time is lager than lastMessageTime
+                var right = results.indexFirstBE(lastMessageTime) { it.msgHead.msgTime.toLong() }
+                // check messages with same time
+                if (results[right].msgHead.msgTime.toLong() == lastMessageTime) {
+                    do { right ++ } while (right <= results.size - 1 && results[right].msgHead.msgTime <= lastMessageTime)
+                }
+                // loops at most 20 times, just traverse
+                val left = results.indexOfFirst { it.msgHead.msgTime >= timeStart }
+
+                MessageSvcPbGetRoamMsgReq.Response(
+                    if (left == right) null else results.subList(left, right),
+                    if (left == right) -1L else results[right - 1].msgHead.msgTime.toLong(), -1L, byteArrayOf()
+                )
+            }
+            is TroopManagement.GetGroupLastMsgSeq.Response.Failed -> {
+                MessageSvcPbGetRoamMsgReq.Response(null, -1L, -1L, byteArrayOf())
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/utils/collection.kt b/mirai-core/src/commonMain/kotlin/utils/collection.kt
new file mode 100644
index 000000000..f8b827ce0
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/utils/collection.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019-2022 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.utils
+
+internal fun <T, M : Comparable<M>> List<T>.indexFirstBE(
+    value: M,
+    mapping: (T) -> M
+): Int {
+    var (left, right) = 0 to size - 1
+    var index = -1
+    while (left <= right) {
+        val middle = left + (right - left) / 2
+        if (mapping(get(middle)) >= value) {
+            index = middle
+            right = middle - 1
+        } else {
+            left = middle + 1
+        }
+    }
+    return index
+}
\ No newline at end of file