Browse Source

支持获取消息记录 (漫游消息) (#1520)

* Support roaming messages for friend

* Remove `RoamingMessage.getMessage`

* Remove `RoamingAuthenticator`

* Mark `@JavaFriendlyAPI`

* apiDump

Co-authored-by: Karlatemp <karlatemp@vip.qq.com>
Him188 3 years ago
parent
commit
ef301cc6e5

+ 52 - 1
binary-compatibility-validator/android/api/binary-compatibility-validator-android.api

@@ -331,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
 	public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
 	public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
 }
 }
 
 
-public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User {
+public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/roaming/RoamingSupported {
 	public fun delete ()V
 	public fun delete ()V
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
@@ -748,6 +748,57 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 }
 }
 
 
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
+	public fun getBot ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
+	public abstract fun getIds ()[I
+	public abstract fun getInternalIds ()[I
+	public abstract fun getSender ()J
+	public abstract fun getTarget ()J
+	public abstract fun getTime ()J
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessageFilter {
+	public static final field ANY Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public static final field Companion Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter$Companion;
+	public static final field RECEIVED Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public static final field SENT Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public fun and (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public abstract fun invoke (Lnet/mamoe/mirai/contact/roaming/RoamingMessage;)Z
+	public fun not ()Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public fun or (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+}
+
+public final class net/mamoe/mirai/contact/roaming/RoamingMessageFilter$Companion {
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessages {
+	public fun getAllMessages (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lkotlinx/coroutines/flow/Flow;
+	public fun getAllMessages (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getAllMessages$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
+	public static synthetic fun getAllMessages$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getAllMessagesStream ()Ljava/util/stream/Stream;
+	public fun getAllMessagesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun getAllMessagesStream (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Ljava/util/stream/Stream;
+	public fun getAllMessagesStream (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getAllMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Ljava/util/stream/Stream;
+	public static synthetic fun getAllMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getMessagesIn (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lkotlinx/coroutines/flow/Flow;
+	public abstract fun getMessagesIn (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getMessagesIn$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
+	public static synthetic fun getMessagesIn$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getMessagesStream (JJ)Ljava/util/stream/Stream;
+	public fun getMessagesStream (JJLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun getMessagesStream (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Ljava/util/stream/Stream;
+	public abstract fun getMessagesStream (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Ljava/util/stream/Stream;
+	public static synthetic fun getMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingSupported : net/mamoe/mirai/contact/Contact {
+	public abstract fun getRoamingMessages ()Lnet/mamoe/mirai/contact/roaming/RoamingMessages;
+}
+
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;

+ 52 - 1
binary-compatibility-validator/api/binary-compatibility-validator.api

@@ -331,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
 	public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
 	public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile;
 }
 }
 
 
-public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User {
+public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/roaming/RoamingSupported {
 	public fun delete ()V
 	public fun delete ()V
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
@@ -748,6 +748,57 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 }
 }
 
 
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
+	public fun getBot ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
+	public abstract fun getIds ()[I
+	public abstract fun getInternalIds ()[I
+	public abstract fun getSender ()J
+	public abstract fun getTarget ()J
+	public abstract fun getTime ()J
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessageFilter {
+	public static final field ANY Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public static final field Companion Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter$Companion;
+	public static final field RECEIVED Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public static final field SENT Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public fun and (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public abstract fun invoke (Lnet/mamoe/mirai/contact/roaming/RoamingMessage;)Z
+	public fun not ()Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+	public fun or (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;
+}
+
+public final class net/mamoe/mirai/contact/roaming/RoamingMessageFilter$Companion {
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessages {
+	public fun getAllMessages (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lkotlinx/coroutines/flow/Flow;
+	public fun getAllMessages (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getAllMessages$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
+	public static synthetic fun getAllMessages$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getAllMessagesStream ()Ljava/util/stream/Stream;
+	public fun getAllMessagesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun getAllMessagesStream (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Ljava/util/stream/Stream;
+	public fun getAllMessagesStream (Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getAllMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Ljava/util/stream/Stream;
+	public static synthetic fun getAllMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;Lnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getMessagesIn (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Lkotlinx/coroutines/flow/Flow;
+	public abstract fun getMessagesIn (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getMessagesIn$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
+	public static synthetic fun getMessagesIn$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+	public fun getMessagesStream (JJ)Ljava/util/stream/Stream;
+	public fun getMessagesStream (JJLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun getMessagesStream (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;)Ljava/util/stream/Stream;
+	public abstract fun getMessagesStream (JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static synthetic fun getMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;ILjava/lang/Object;)Ljava/util/stream/Stream;
+	public static synthetic fun getMessagesStream$default (Lnet/mamoe/mirai/contact/roaming/RoamingMessages;JJLnet/mamoe/mirai/contact/roaming/RoamingMessageFilter;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/contact/roaming/RoamingSupported : net/mamoe/mirai/contact/Contact {
+	public abstract fun getRoamingMessages ()Lnet/mamoe/mirai/contact/roaming/RoamingMessages;
+}
+
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getNick ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;
 	public abstract fun getRemark ()Ljava/lang/String;

+ 2 - 1
mirai-core-api/src/commonMain/kotlin/contact/Friend.kt

@@ -15,6 +15,7 @@ package net.mamoe.mirai.contact
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.roaming.RoamingSupported
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.action.FriendNudge
 import net.mamoe.mirai.message.action.FriendNudge
@@ -34,7 +35,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
  * @see FriendMessageEvent
  * @see FriendMessageEvent
  */
  */
 @NotStableForInheritance
 @NotStableForInheritance
-public interface Friend : User, CoroutineScope, AudioSupported {
+public interface Friend : User, CoroutineScope, AudioSupported, RoamingSupported {
     /**
     /**
      * 向这个对象发送消息.
      * 向这个对象发送消息.
      *
      *

+ 90 - 0
mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessageFilter.kt

@@ -0,0 +1,90 @@
+/*
+ * 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.contact.roaming
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.message.data.MessageSource
+
+/**
+ * @since 2.8
+ */
+public fun interface RoamingMessageFilter {
+    public operator fun invoke(roamingMessage: RoamingMessage): Boolean
+
+
+    public infix fun and(other: RoamingMessageFilter): RoamingMessageFilter {
+        return RoamingMessageFilter { this.invoke(it) && other.invoke(it) }
+    }
+
+    public infix fun or(other: RoamingMessageFilter): RoamingMessageFilter {
+        return RoamingMessageFilter { this.invoke(it) || other.invoke(it) }
+    }
+
+    public fun not(): RoamingMessageFilter {
+        return RoamingMessageFilter { !this.invoke(it) }
+    }
+
+
+    public companion object {
+        /**
+         * 筛选任何消息 (相当于不筛选)
+         */
+        @JvmField
+        public val ANY: RoamingMessageFilter = RoamingMessageFilter { true }
+
+        /**
+         * 筛选 bot 接收的消息
+         */
+        @JvmField
+        public val RECEIVED: RoamingMessageFilter = RoamingMessageFilter { it.sender != it.bot.id }
+
+        /**
+         * 筛选 bot 发送的消息
+         */
+        @JvmField
+        public val SENT: RoamingMessageFilter = RoamingMessageFilter { it.sender == it.bot.id }
+    }
+}
+
+/**
+ * 还未解析的漫游消息.
+ *
+ * @since 2.8
+ */
+public interface RoamingMessage {
+    public val contact: Contact
+    public val bot: Bot get() = contact.bot
+
+    /**
+     * 发送人 id
+     */
+    public val sender: Long
+
+    /**
+     * 收信人或群的 id
+     */
+    public val target: Long
+
+    /**
+     * 时间戳, 单位为秒, 服务器时间.
+     */
+    public val time: Long
+
+    /**
+     * @see MessageSource.ids
+     */
+    public val ids: IntArray
+
+    /**
+     * @see MessageSource.internalIds
+     */
+    public val internalIds: IntArray
+}

+ 101 - 0
mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt

@@ -0,0 +1,101 @@
+/*
+ * 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
+ */
+
+@file:JvmBlockingBridge
+
+package net.mamoe.mirai.contact.roaming
+
+import kotlinx.coroutines.flow.Flow
+import net.mamoe.kjbb.JvmBlockingBridge
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.utils.JavaFriendlyAPI
+import java.util.stream.Stream
+
+/**
+ * 漫游消息记录管理器.
+ *
+ * @since 2.8
+ * @see RoamingSupported
+ */
+public interface RoamingMessages {
+    ///////////////////////////////////////////////////////////////////////////
+    // Get list
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 查询指定时间段内的漫游消息记录.
+     *
+     * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息.
+     * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回.
+     *
+     * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响.
+     *
+     * @param timeStart 起始时间, UTC+8 时间戳, 单位为秒. 可以为 `0`, 即表示从可以获取的最早的消息起. 负数将会被看是 `0`.
+     * @param timeEnd 结束时间, UTC+8 时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值.
+     * @param filter 过滤器.
+     */
+    public suspend fun getMessagesIn(
+        timeStart: Long,
+        timeEnd: Long,
+        filter: RoamingMessageFilter? = null
+    ): Flow<MessageChain>
+
+    /**
+     * 查询所有漫游消息记录.
+     *
+     * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息.
+     * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回.
+     *
+     * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响.
+     *
+     * @param filter 过滤器.
+     */
+    public suspend fun getAllMessages(
+        filter: RoamingMessageFilter? = null
+    ): Flow<MessageChain> = getMessagesIn(0, Long.MAX_VALUE, filter)
+
+    /**
+     * 查询指定时间段内的漫游消息记录.
+     *
+     * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息.
+     * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回.
+     *
+     * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响.
+     *
+     * @param timeStart 起始时间, UTC+8 时间戳, 单位为秒. 可以为 `0`, 即表示从可以获取的最早的消息起. 负数将会被看是 `0`.
+     * @param timeEnd 结束时间, UTC+8 时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值.
+     * @param filter 过滤器.
+     */
+    @Suppress("OVERLOADS_INTERFACE")
+    @JvmOverloads
+    @JavaFriendlyAPI
+    public suspend fun getMessagesStream(
+        timeStart: Long,
+        timeEnd: Long,
+        filter: RoamingMessageFilter? = null
+    ): Stream<MessageChain>
+
+    /**
+     * 查询所有漫游消息记录.
+     *
+     * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息.
+     * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回.
+     *
+     * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响.
+     *
+     * @param filter 过滤器.
+     */
+    @Suppress("OVERLOADS_INTERFACE")
+    @JvmOverloads
+    @JavaFriendlyAPI
+    public suspend fun getAllMessagesStream(
+        filter: RoamingMessageFilter? = null
+    ): Stream<MessageChain> = getMessagesStream(0, Long.MAX_VALUE, filter)
+}

+ 26 - 0
mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingSupported.kt

@@ -0,0 +1,26 @@
+/*
+ * 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
+ */
+
+@file:JvmBlockingBridge
+
+package net.mamoe.mirai.contact.roaming
+
+import net.mamoe.kjbb.JvmBlockingBridge
+import net.mamoe.mirai.contact.Contact
+
+/**
+ * 支持查询漫游消息记录的 [Contact].
+ * @since 2.8
+ */
+public interface RoamingSupported : Contact {
+    /**
+     * 获取漫游消息记录管理器.
+     */
+    public val roamingMessages: RoamingMessages
+}

+ 6 - 2
mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt

@@ -17,10 +17,12 @@ package net.mamoe.mirai.internal.contact
 import net.mamoe.mirai.LowLevelApi
 import net.mamoe.mirai.LowLevelApi
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.contact.Friend
 import net.mamoe.mirai.contact.Friend
+import net.mamoe.mirai.contact.roaming.RoamingMessages
 import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
 import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
 import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
 import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
 import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
+import net.mamoe.mirai.internal.contact.roaming.RoamingMessagesImplFriend
 import net.mamoe.mirai.internal.message.OfflineAudioImpl
 import net.mamoe.mirai.internal.message.OfflineAudioImpl
 import net.mamoe.mirai.internal.network.highway.*
 import net.mamoe.mirai.internal.network.highway.*
 import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346
 import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346
@@ -61,8 +63,8 @@ internal class FriendImpl(
     override val info: FriendInfoImpl,
     override val info: FriendInfoImpl,
 ) : Friend, AbstractUser(bot, parentCoroutineContext, info) {
 ) : Friend, AbstractUser(bot, parentCoroutineContext, info) {
     override suspend fun delete() {
     override suspend fun delete() {
-        check(bot.friends[this.id] != null) {
-            "Friend ${this.id} had already been deleted"
+        check(bot.friends[id] != null) {
+            "Friend $id had already been deleted"
         }
         }
         bot.network.run {
         bot.network.run {
             FriendList.DelFriend.invoke(bot.client, this@FriendImpl).sendAndExpect().also {
             FriendList.DelFriend.invoke(bot.client, this@FriendImpl).sendAndExpect().also {
@@ -145,4 +147,6 @@ internal class FriendImpl(
 
 
         return audio!!
         return audio!!
     }
     }
+
+    override val roamingMessages: RoamingMessages by lazy { RoamingMessagesImplFriend(this) }
 }
 }

+ 137 - 0
mirai-core/src/commonMain/kotlin/contact/roaming/RoamingMessagesImpl.kt

@@ -0,0 +1,137 @@
+/*
+ * 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
+ */
+
+@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+package net.mamoe.mirai.internal.contact.roaming
+
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.contact.Contact
+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.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.receive.MessageSvcPbGetRoamMsgReq
+import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.utils.*
+import java.util.stream.Stream
+
+internal sealed class RoamingMessagesImpl : RoamingMessages {
+    abstract val contact: AbstractContact
+
+    override suspend fun getMessagesIn(
+        timeStart: Long,
+        timeEnd: Long,
+        filter: RoamingMessageFilter?
+    ): Flow<MessageChain> {
+        return flow {
+            var lastMessageTime = timeEnd.coerceAtLeast(timeStart).coerceAtLeast(1)
+            var random = 0L
+            while (currentCoroutineContext().isActive) {
+                val resp = requestRoamMsg(timeStart, lastMessageTime, random)
+                val messages = resp.messages ?: break
+                if (filter == null || filter === RoamingMessageFilter.ANY) {
+                    // fast path
+                    messages.forEach { emit(it.toMessageChainOnline(contact.bot)) }
+                } else {
+                    for (message in messages) {
+                        if (filter.invoke(createRoamingMessage(message, messages))) {
+                            emit(message.toMessageChainOnline(contact.bot))
+                        }
+                    }
+                }
+
+                lastMessageTime = resp.lastMessageTime
+                random = resp.random
+            }
+        }
+    }
+
+    private fun createRoamingMessage(
+        message: MsgComm.Msg,
+        messages: List<MsgComm.Msg>
+    ) = object : RoamingMessage {
+        override val contact: Contact get() = this@RoamingMessagesImpl.contact
+        override val sender: Long get() = message.msgHead.fromUin
+        override val target: Long
+            get() = message.msgHead.groupInfo?.groupCode ?: message.msgHead.toUin
+        override val time: Long get() = message.msgHead.msgTime.toLongUnsigned()
+        override val ids: IntArray by lazy { messages.mapToIntArray { it.msgHead.msgSeq } }
+        override val internalIds: IntArray by lazy {
+            messages.mapToIntArray { it.msgBody.richText.attr?.random ?: 0 } // other client 消息的这个是0
+        }
+    }
+
+
+    @JavaFriendlyAPI
+    override suspend fun getMessagesStream(
+        timeStart: Long,
+        timeEnd: Long,
+        filter: RoamingMessageFilter?,
+    ): Stream<MessageChain> {
+        return stream {
+            var lastMessageTime = timeEnd
+            var random = 0L
+            while (true) {
+                val resp = runBlocking {
+                    requestRoamMsg(timeStart, lastMessageTime, random)
+                }
+
+                val messages = resp.messages ?: break
+                if (filter == null || filter === RoamingMessageFilter.ANY) {
+                    messages.forEach { yield(runBlocking { it.toMessageChainOnline(contact.bot) }) }
+                } else {
+                    for (message in messages) {
+                        if (filter.invoke(createRoamingMessage(message, messages))) {
+                            yield(runBlocking { message.toMessageChainOnline(contact.bot) })
+                        }
+                    }
+                }
+
+                lastMessageTime = resp.lastMessageTime
+                random = resp.random
+            }
+        }
+    }
+
+    abstract suspend fun requestRoamMsg(
+        timeStart: Long,
+        lastMessageTime: Long,
+        random: Long
+    ): MessageSvcPbGetRoamMsgReq.Response
+}
+
+internal class RoamingMessagesImplFriend(
+    override val contact: FriendImpl
+) : RoamingMessagesImpl() {
+    override suspend fun requestRoamMsg(
+        timeStart: Long,
+        lastMessageTime: Long,
+        random: Long
+    ): MessageSvcPbGetRoamMsgReq.Response {
+        return MessageSvcPbGetRoamMsgReq.createForFriend(
+            client = contact.bot.client,
+            uin = contact.id,
+            timeStart = timeStart,
+            lastMsgTime = lastMessageTime,
+            random = random,
+            maxCount = 1000,
+            sig = byteArrayOf(),
+            pwd = byteArrayOf()
+        ).sendAndExpect(contact.bot).value.check()
+    }
+}

+ 21 - 0
mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt

@@ -50,6 +50,27 @@ internal suspend fun List<MsgComm.Msg>.toMessageChainOnline(
     return toMessageChain(bot, groupIdOrZero, true, messageSourceKind).refineDeep(bot, refineContext)
     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(
 //internal fun List<MsgComm.Msg>.toMessageChainOffline(
 //    bot: Bot,
 //    bot: Bot,
 //    groupIdOrZero: Long,
 //    groupIdOrZero: Long,

+ 1 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt

@@ -135,6 +135,7 @@ internal object KnownPacketFactories {
         MessageSvcPushForceOffline,
         MessageSvcPushForceOffline,
         MessageSvcPbSendMsg,
         MessageSvcPbSendMsg,
         MessageSvcPbDeleteMsg,
         MessageSvcPbDeleteMsg,
+        MessageSvcPbGetRoamMsgReq,
         FriendList.GetFriendGroupList,
         FriendList.GetFriendGroupList,
         FriendList.DelFriend,
         FriendList.DelFriend,
         FriendList.GetTroopListSimplify,
         FriendList.GetTroopListSimplify,

+ 91 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetRoamMsgReq.kt

@@ -0,0 +1,91 @@
+/*
+ * 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.network.protocol.packet.chat.receive
+
+import kotlinx.io.core.ByteReadPacket
+import net.mamoe.mirai.internal.QQAndroidBot
+import net.mamoe.mirai.internal.network.Packet
+import net.mamoe.mirai.internal.network.QQAndroidClient
+import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
+import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc
+import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
+import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
+import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
+import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.utils.Either
+import net.mamoe.mirai.utils.Either.Companion.mapRight
+import net.mamoe.mirai.utils.FailureResponse
+import net.mamoe.mirai.utils.checked
+
+internal class CheckedResponse<T>(
+    val value: Either<FailureResponse, T>
+) : Packet
+
+internal object MessageSvcPbGetRoamMsgReq :
+    OutgoingPacketFactory<CheckedResponse<MessageSvcPbGetRoamMsgReq.Response>>("MessageSvc.PbGetRoamMsg") {
+
+    class Response(
+        val messages: List<MsgComm.Msg>?,
+        val lastMessageTime: Long,
+        val random: Long,
+        val sig: ByteArray, // 似乎没被用到, 服务器每次返回不同
+    ) {
+
+    }
+
+    fun createForFriend(
+        client: QQAndroidClient,
+        uin: Long,
+        timeStart: Long,
+        lastMsgTime: Long, // 上次 resp 中的, 否则为期待的 time end
+        random: Long = 0,
+        maxCount: Int = 1000,
+        sig: ByteArray = EMPTY_BYTE_ARRAY, // 客户端每次请求相同
+        pwd: ByteArray = EMPTY_BYTE_ARRAY,
+    ) = buildOutgoingUniPacket(client) {
+        writeProtoBuf(
+            MsgSvc.PbGetRoamMsgReq.serializer(), MsgSvc.PbGetRoamMsgReq(
+                peerUin = uin,
+                beginMsgtime = timeStart,
+                lastMsgtime = lastMsgTime,
+                checkPwd = 1, // always
+                readCnt = maxCount,
+                subcmd = 1,
+                reqType = 1,
+                sig = sig,
+                pwd = pwd,
+                random = random,
+            )
+        )
+    }
+
+    override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CheckedResponse<Response> {
+        val resp = readProtoBuf(MsgSvc.PbGetRoamMsgResp.serializer())
+        if (resp.result == 1) return CheckedResponse(
+            Either.right(
+                Response(
+                    null,
+                    resp.lastMsgtime,
+                    resp.random,
+                    resp.sig,
+                )
+            )
+        ) // finished
+        return CheckedResponse(resp.checked().mapRight {
+            Response(
+                messages = resp.msg.asReversed(),
+                lastMessageTime = resp.lastMsgtime,
+                random = resp.random,
+                sig = resp.sig,
+            )
+        })
+    }
+}