From 66f462029253477ea5e380ffb714b0a8faebce91 Mon Sep 17 00:00:00 2001
From: Him188 <Him188@mamoe.net>
Date: Wed, 22 Apr 2020 18:35:30 +0800
Subject: [PATCH] Add `MessageSource.internalId`, support
 `OfflineMessageSource` building

---
 .../mirai/qqandroid/QQAndroidBot.common.kt    |  53 +++-
 .../qqandroid/message/incomingSourceImpl.kt   |  12 +-
 .../qqandroid/message/offlineSourceImpl.kt    |   4 +-
 .../qqandroid/message/outgoingSourceImpl.kt   |  10 +-
 .../packet/chat/receive/MessageSvc.kt         |  12 +-
 .../commonMain/kotlin/net.mamoe.mirai/Bot.kt  |  19 ++
 .../message/data/CustomMessage.kt             |  12 +-
 .../net.mamoe.mirai/message/data/Message.kt   |   1 +
 .../message/data/MessageSource.kt             |   7 +-
 .../message/data/OfflineMessageSource.kt      | 258 ++++++++++++++----
 10 files changed, 310 insertions(+), 78 deletions(-)

diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
index a2ef510a9..b1127dd1e 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt
@@ -35,6 +35,7 @@ import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.MemberJoinRequestEvent
 import net.mamoe.mirai.event.events.MessageRecallEvent
 import net.mamoe.mirai.event.events.NewFriendRequestEvent
+import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.network.LoginFailedException
@@ -45,6 +46,7 @@ import net.mamoe.mirai.qqandroid.message.*
 import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
 import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
@@ -321,7 +323,7 @@ internal abstract class QQAndroidBotBase constructor(
                         bot.asQQAndroidBot().client,
                         group.id,
                         source.sequenceId,
-                        source.random
+                        source.internalId
                     ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
                 }
             }
@@ -335,7 +337,7 @@ internal abstract class QQAndroidBotBase constructor(
                     bot.client,
                     source.targetId,
                     source.sequenceId,
-                    source.random,
+                    source.internalId,
                     source.time
                 ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
             }
@@ -351,7 +353,7 @@ internal abstract class QQAndroidBotBase constructor(
                     source.target.group.id,
                     source.targetId,
                     source.sequenceId,
-                    source.random,
+                    source.internalId,
                     source.time
                 ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
             }
@@ -365,7 +367,7 @@ internal abstract class QQAndroidBotBase constructor(
                             bot.client,
                             source.targetId,
                             source.sequenceId,
-                            source.random,
+                            source.internalId,
                             source.time
                         ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
                     }
@@ -378,7 +380,7 @@ internal abstract class QQAndroidBotBase constructor(
                             source.targetId, // groupUin
                             source.targetId, // memberUin
                             source.sequenceId,
-                            source.random,
+                            source.internalId,
                             source.time
                         ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
                     }
@@ -387,7 +389,7 @@ internal abstract class QQAndroidBotBase constructor(
                             bot.client,
                             source.targetId,
                             source.sequenceId,
-                            source.random
+                            source.internalId
                         ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
                     }
                 }
@@ -658,6 +660,43 @@ internal abstract class QQAndroidBotBase constructor(
         else -> error("unsupported image class: ${image::class.simpleName}")
     }
 
+    override fun constructMessageSource(
+        kind: OfflineMessageSource.Kind,
+        fromUin: Long,
+        targetUin: Long,
+        id: Int,
+        time: Int,
+        internalId: Int,
+        originalMessage: MessageChain
+    ): OfflineMessageSource {
+        return object : OfflineMessageSource(), MessageSourceInternal {
+            override val kind: Kind get() = kind
+            override val id: Int get() = id
+            override val bot: Bot get() = this@QQAndroidBotBase
+            override val time: Int get() = time
+            override val fromId: Long get() = fromUin
+            override val targetId: Long get() = targetUin
+            override val originalMessage: MessageChain get() = originalMessage
+            override val sequenceId: Int = id
+            override val internalId: Int = internalId
+            override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+
+            override fun toJceData(): ImMsgBody.SourceMsg {
+                return ImMsgBody.SourceMsg(
+                    origSeqs = listOf(sequenceId),
+                    senderUin = fromUin,
+                    toUin = 0,
+                    flag = 1,
+                    elems = originalMessage.toRichTextElems(forGroup = kind == Kind.GROUP, withGeneralFlags = false),
+                    type = 0,
+                    time = time,
+                    pbReserve = EMPTY_BYTE_ARRAY,
+                    srcMsg = EMPTY_BYTE_ARRAY
+                )
+            }
+        }
+    }
+
     @Suppress("DeprecatedCallableAddReplaceWith")
     @PlannedRemoval("1.0.0")
     @Deprecated("use your own Http clients, this is going to be removed in 1.0.0", level = DeprecationLevel.WARNING)
@@ -673,6 +712,8 @@ internal abstract class QQAndroidBotBase constructor(
             .fold(5381) { acc: Int, b: Byte -> acc + acc.shl(5) + b.toInt() }
 }
 
+internal val EMPTY_BYTE_ARRAY = ByteArray(0)
+
 @Suppress("DEPRECATION")
 @OptIn(MiraiInternalAPI::class)
 internal expect fun io.ktor.utils.io.ByteReadChannel.toKotlinByteReadChannel(): ByteReadChannel
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/incomingSourceImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/incomingSourceImpl.kt
index 3ee4741b6..de69d5f4d 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/incomingSourceImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/incomingSourceImpl.kt
@@ -27,7 +27,7 @@ import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
 
 internal interface MessageSourceInternal {
     val sequenceId: Int
-    val random: Int
+    val internalId: Int // randomId
 
     @Deprecated("don't use this internally. Use sequenceId or random instead.", level = DeprecationLevel.ERROR)
     val id: Int
@@ -54,12 +54,12 @@ internal class MessageSourceFromFriendImpl(
     override val sequenceId: Int get() = msg.msgHead.msgSeq
     override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
     override val id: Int get() = sequenceId// msg.msgBody.richText.attr!!.random
-    override val random: Int get() = msg.msgBody.richText.attr!!.random
+    override val internalId: Int get() = msg.msgBody.richText.attr!!.random
     override val time: Int get() = msg.msgHead.msgTime
     override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, 0, false) }
     override val sender: Friend get() = bot.getFriend(msg.msgHead.fromUin)
 
-    private val jceData by lazy { msg.toJceDataFriendOrTemp(random) }
+    private val jceData by lazy { msg.toJceDataFriendOrTemp(internalId) }
 
     override fun toJceData(): ImMsgBody.SourceMsg = jceData
 }
@@ -105,14 +105,14 @@ internal class MessageSourceFromTempImpl(
     private val msg: MsgComm.Msg
 ) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
     override val sequenceId: Int get() = msg.msgHead.msgSeq
-    override val random: Int get() = msg.msgBody.richText.attr!!.random
+    override val internalId: Int get() = msg.msgBody.richText.attr!!.random
     override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
     override val id: Int get() = sequenceId//
     override val time: Int get() = msg.msgHead.msgTime
     override val originalMessage: MessageChain by lazy { msg.toMessageChain(bot, 0, false) }
     override val sender: Member get() = with(msg.msgHead) { bot.getGroup(c2cTmpMsgHead!!.groupUin)[fromUin] }
 
-    private val jceData by lazy { msg.toJceDataFriendOrTemp(random) }
+    private val jceData by lazy { msg.toJceDataFriendOrTemp(internalId) }
     override fun toJceData(): ImMsgBody.SourceMsg = jceData
 }
 
@@ -122,7 +122,7 @@ internal data class MessageSourceFromGroupImpl(
 ) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
     override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
     override val sequenceId: Int get() = msg.msgHead.msgSeq
-    override val random: Int get() = msg.msgBody.richText.attr!!.random
+    override val internalId: Int get() = msg.msgBody.richText.attr!!.random
     override val id: Int get() = sequenceId
     override val time: Int get() = msg.msgHead.msgTime
     override val originalMessage: MessageChain by lazy {
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/offlineSourceImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/offlineSourceImpl.kt
index 8e44f6851..43e79cd58 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/offlineSourceImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/offlineSourceImpl.kt
@@ -28,7 +28,7 @@ internal class OfflineMessageSourceImplByMsg(
 ) : OfflineMessageSource(), MessageSourceInternal {
     override val kind: Kind = if (delegate.msgHead.groupInfo != null) Kind.GROUP else Kind.FRIEND
     override val id: Int get() = sequenceId
-    override val random: Int
+    override val internalId: Int
         get() = delegate.msgHead.msgUid.toInt()
     override val time: Int
         get() = delegate.msgHead.msgTime
@@ -74,7 +74,7 @@ internal class OfflineMessageSourceImplBySourceMsg(
     override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
     override val sequenceId: Int
         get() = delegate.origSeqs?.first() ?: error("cannot find sequenceId")
-    override val random: Int
+    override val internalId: Int
         get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids?.toInt() ?: 0
     override val time: Int get() = delegate.time
     override val originalMessage: MessageChain by lazy { delegate.toMessageChain(bot, groupIdOrZero) }
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/outgoingSourceImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/outgoingSourceImpl.kt
index 832ec4eff..1ea7feb39 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/outgoingSourceImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/outgoingSourceImpl.kt
@@ -33,7 +33,7 @@ private fun <T> T.toJceDataImpl(): ImMsgBody.SourceMsg
         where T : MessageSourceInternal, T : MessageSource {
 
     val elements = originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
-    val messageUid: Long = sequenceId.toLong().shl(32) or random.toLong().and(0xffFFffFF)
+    val messageUid: Long = sequenceId.toLong().shl(32) or internalId.toLong().and(0xffFFffFF)
     return ImMsgBody.SourceMsg(
         origSeqs = listOf(sequenceId),
         senderUin = fromId,
@@ -70,7 +70,7 @@ private fun <T> T.toJceDataImpl(): ImMsgBody.SourceMsg
 
 internal class MessageSourceToFriendImpl(
     override val sequenceId: Int,
-    override val random: Int,
+    override val internalId: Int,
     override val time: Int,
     override val originalMessage: MessageChain,
     override val sender: Bot,
@@ -87,7 +87,7 @@ internal class MessageSourceToFriendImpl(
 
 internal class MessageSourceToTempImpl(
     override val sequenceId: Int,
-    override val random: Int,
+    override val internalId: Int,
     override val time: Int,
     override val originalMessage: MessageChain,
     override val sender: Bot,
@@ -104,7 +104,7 @@ internal class MessageSourceToTempImpl(
 
 internal class MessageSourceToGroupImpl(
     coroutineScope: CoroutineScope,
-    override val random: Int,
+    override val internalId: Int,
     override val time: Int,
     override val originalMessage: MessageChain,
     override val sender: Bot,
@@ -120,7 +120,7 @@ internal class MessageSourceToGroupImpl(
         coroutineScope.asyncFromEventOrNull<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int>(
             timeoutMillis = 3000
         ) {
-            if (it.messageRandom == this@MessageSourceToGroupImpl.random) {
+            if (it.messageRandom == this@MessageSourceToGroupImpl.internalId) {
                 it.sequenceId
             } else null
         }
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
index 9a4562364..594f2e8b5 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
@@ -369,7 +369,7 @@ internal class MessageSvc {
         ): OutgoingPacket {
             val rand = Random.nextInt().absoluteValue
             val source = MessageSourceToFriendImpl(
-                random = rand,
+                internalId = rand,
                 sender = client.bot,
                 target = qq,
                 time = currentTimeSeconds.toInt(),
@@ -403,7 +403,7 @@ internal class MessageSvc {
                         )
                     ),
                     msgSeq = source.sequenceId,
-                    msgRand = source.random,
+                    msgRand = source.internalId,
                     syncCookie = SyncCookie(time = source.time.toLong()).toByteArray(SyncCookie.serializer())
                     // msgVia = 1
                 )
@@ -418,7 +418,7 @@ internal class MessageSvc {
             sourceCallback: (MessageSourceToTempImpl) -> Unit
         ): OutgoingPacket {
             val source = MessageSourceToTempImpl(
-                random = Random.nextInt().absoluteValue,
+                internalId = Random.nextInt().absoluteValue,
                 sender = client.bot,
                 target = member,
                 time = currentTimeSeconds.toInt(),
@@ -451,7 +451,7 @@ internal class MessageSvc {
                         )
                     ),
                     msgSeq = source.sequenceId,
-                    msgRand = source.random,
+                    msgRand = source.internalId,
                     syncCookie = SyncCookie(time = source.time.toLong()).toByteArray(SyncCookie.serializer())
                 )
             )
@@ -467,7 +467,7 @@ internal class MessageSvc {
 
             val source = MessageSourceToGroupImpl(
                 group,
-                random = Random.nextInt().absoluteValue,
+                internalId = Random.nextInt().absoluteValue,
                 sender = client.bot,
                 target = group,
                 time = currentTimeSeconds.toInt(),
@@ -503,7 +503,7 @@ internal class MessageSvc {
                         )
                     ),
                     msgSeq = client.atomicNextMessageSequenceId(),
-                    msgRand = source.random,
+                    msgRand = source.internalId,
                     syncCookie = EMPTY_BYTE_ARRAY,
                     msgVia = 1
                 )
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt
index a244fcf24..a69edc475 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt
@@ -179,6 +179,24 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI(
     @JvmSynthetic
     abstract suspend fun queryImageUrl(image: Image): String
 
+    /**
+     * 构造一个 [OfflineMessageSource]
+     *
+     * @param id 即 [MessageSource.id]
+     * @param internalId 即 [MessageSource.internalId]
+     *
+     * @param fromUin 为用户时为 [Friend.id], 为群时需使用 [Group.calculateGroupUinByGroupCode] 计算
+     * @param targetUin 为用户时为 [Friend.id], 为群时需使用 [Group.calculateGroupUinByGroupCode] 计算
+     */
+    @MiraiExperimentalAPI
+    @SinceMirai("0.39.0")
+    abstract fun constructMessageSource(
+        kind: OfflineMessageSource.Kind,
+        fromUin: Long, targetUin: Long,
+        id: Int, time: Int, internalId: Int,
+        originalMessage: MessageChain
+    ): OfflineMessageSource
+
     /**
      * 获取图片下载链接并开始下载.
      *
@@ -281,6 +299,7 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI(
     val selfQQDeprecated: QQ
         get() = selfQQ
 
+    @PlannedRemoval("1.0.0.")
     @JvmName("getFriend")
     @Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION_ERROR")
     @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN)
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt
index 4aae456f3..89ef02e51 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt
@@ -28,11 +28,9 @@ import net.mamoe.mirai.utils.*
  *
  * 目前在回复时无法通过 [originalMessage] 获取自定义类型消息
  *
- * **实现方法**:
- *
  * @sample samples.CustomMessageIdentifier 实现示例
  *
- * @see CustomMessageMetadata
+ * @see CustomMessageMetadata 自定义消息元数据
  */
 @SinceMirai("0.38.0")
 @MiraiExperimentalAPI
@@ -172,6 +170,11 @@ sealed class CustomMessage : SingleMessage {
 /**
  * 自定义消息元数据.
  *
+ * **实现方法**:
+ * 1. 实现一个类继承 [CustomMessageMetadata], 添加 `@Serializable` (来自 `kotlinx.serialization`)
+ * 2. 添加伴生对象, 继承 [CustomMessage.ProtoBufSerializerFactory] 或 [CustomMessage.JsonSerializerFactory], 或 [CustomMessage.Factory]
+ * 3. 在需要解析消息前调用一次伴生对象以注册
+ *
  * @see CustomMessage 查看更多信息
  * @see ConstrainSingle 可实现此接口以保证消息链中只存在一个元素
  */
@@ -191,8 +194,9 @@ abstract class CustomMessageMetadata : CustomMessage(), MessageMetadata {
 }
 
 
+@Suppress("NOTHING_TO_INLINE")
 @OptIn(MiraiExperimentalAPI::class)
-internal fun <T : CustomMessageMetadata> T.customToStringImpl(factory: CustomMessage.Factory<*>): ByteArray {
+internal inline fun <T : CustomMessageMetadata> T.customToStringImpl(factory: CustomMessage.Factory<*>): ByteArray {
     @Suppress("UNCHECKED_CAST")
     return (factory as CustomMessage.Factory<T>).serialize(this)
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
index 7367e3012..7a5ae3a70 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
@@ -58,6 +58,7 @@ import kotlin.jvm.JvmSynthetic
  * @see QuoteReply 一条消息的引用
  * @see RichMessage 富文本消息, 如 [Xml][XmlMessage], [小程序][LightApp], [Json][JsonMessage]
  * @see HummerMessage 一些特殊的消息, 如 [闪照][FlashImage], [戳一戳][PokeMessage]
+ * @see CustomMessage 自定义消息类型
  *
  * @see MessageChain 消息链(即 `List<Message>`)
  * @see buildMessageChain 构造一个 [MessageChain]
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
index b48693c6d..e0f33371c 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
@@ -62,8 +62,11 @@ sealed class MessageSource : Message, MessageMetadata, ConstrainSingle<MessageSo
 
     /**
      * 内部 id, 仅用于 [Bot.constructMessageSource]
-     * 可能为 0, 取决于服务器是否提供.
+     * 值没有顺序, 也可能为 0, 取决于服务器是否提供.
+     *
+     * 仅用于协议实现.
      */
+    @SinceMirai("0.39.0")
     abstract val internalId: Int
 
     /**
@@ -94,7 +97,7 @@ sealed class MessageSource : Message, MessageMetadata, ConstrainSingle<MessageSo
     @LazyProperty
     abstract val originalMessage: MessageChain
 
-    final override fun toString(): String = "[mirai:source:$id]"
+    final override fun toString(): String = "[mirai:source:$id,$internalId]"
     final override fun contentToString(): String = ""
 }
 
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/OfflineMessageSource.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/OfflineMessageSource.kt
index fb88ebb58..f363f192f 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/OfflineMessageSource.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/OfflineMessageSource.kt
@@ -13,13 +13,14 @@
 
 package net.mamoe.mirai.message.data
 
-import net.mamoe.mirai.BotImpl
-import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.ContactOrBot
+import net.mamoe.mirai.contact.Friend
 import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
-import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.SinceMirai
-import net.mamoe.mirai.utils.asSequence
+import net.mamoe.mirai.utils.currentTimeSeconds
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.jvm.JvmSynthetic
@@ -53,68 +54,231 @@ abstract class OfflineMessageSource : MessageSource() {
 }
 
 
-/**
- * 复制这个消息源, 并修改
- */
-@JvmName("copySource")
-inline fun MessageSource.copyAmend(
-    block: MessageSourceBuilder.() -> Unit
-): OfflineMessageSource {
-    return constructMessageSource()
-}
+///////////////
+//// AMEND ////
+///////////////
 
+
+/**
+ * 复制这个消息源, 并以 [block] 修改
+ *
+ * @see buildMessageSource 查看更多说明
+ */
 @MiraiExperimentalAPI
 @SinceMirai("0.39.0")
-@OptIn(MiraiInternalAPI::class)
-fun constructMessageSource(
-    kind: OfflineMessageSource.Kind,
-    fromUin: Long, targetUin: Long,
-    id: Int, time: Int, internalId: Int,
-    originalMessage: MessageChain
-): OfflineMessageSource {
-    val bot = BotImpl.instances.asSequence().mapNotNull { it.get() }.firstOrNull()
-        ?: error("no Bot instance available")
+@JvmName("copySource")
+fun MessageSource.copyAmend(
+    block: MessageSourceAmender.() -> Unit
+): OfflineMessageSource = toMutableOffline().apply(block)
 
-    return bot.constructMessageSource(kind, fromUin, targetUin, id, time, internalId, originalMessage)
+/**
+ * 仅于 [copyAmend] 中修改 [MessageSource]
+ */
+@SinceMirai("0.39.0")
+interface MessageSourceAmender {
+    var kind: OfflineMessageSource.Kind
+    var fromUin: Long
+    var targetUin: Long
+    var id: Int
+    var time: Int
+    var internalId: Int
+
+    var originalMessage: MessageChain
 }
 
+
+///////////////
+//// BUILD ////
+///////////////
+
+
+/**
+ * 构建一个 [OfflineMessageSource]
+ *
+ * ### 参数
+ * 一个 [OfflineMessageSource] 须要以下参数:
+ * - 发送人和发送目标: 通过 [MessageSourceBuilder.sendTo] 设置
+ * - 消息元数据 (即 [MessageSource.id], [MessageSource.internalId], [MessageSource.time])
+ *   元数据用于 [撤回][MessageSource.recall], [引用回复][MessageSource.quote], 和官方客户端定位原消息.
+ *   可通过 [MessageSourceBuilder.id], [MessageSourceBuilder.time], [MessageSourceBuilder.internalId] 设置
+ *   可通过 [MessageSourceBuilder.metadata] 从另一个 [MessageSource] 复制
+ * - 消息内容: 通过 [MessageSourceBuilder.messages] 设置
+ *
+ * ### 性质
+ * - 当两个消息的元数据相同时, 他们在群中会是同一条消息. 可通过此特性决定官方客户端 "定位原消息" 的目标
+ * - 发送人的信息和消息内容会在官方客户端显示在引用回复中.
+ */
+@SinceMirai("0.39.0")
 @JvmSynthetic
 @MiraiExperimentalAPI
-inline fun buildMessageSource(block: MessageSourceBuilder.() -> Unit): MessageSource {
-    val builder = MessageSourceBuilder().apply(block)
+fun Bot.buildMessageSource(block: MessageSourceBuilder.() -> Unit): MessageSource {
+    val builder = MessageSourceBuilderImpl().apply(block)
     return constructMessageSource(
-        builder.kind ?: error("found "),
-        block
+        builder.kind ?: error("You must call `Contact.sendTo(Contact)` when `buildMessageSource`"),
+        builder.fromUin,
+        builder.targetUin,
+        builder.id,
+        builder.time,
+        builder.internalId,
+        builder.originalMessages.build()
     )
 }
 
-@DslMarker
-annotation class SourceBuilderDsl
+/**
+ * @see buildMessageSource
+ */
+abstract class MessageSourceBuilder {
+    internal abstract var kind: OfflineMessageSource.Kind?
+    internal abstract var fromUin: Long
+    internal abstract var targetUin: Long
 
-class MessageSourceBuilder(
-    source: OfflineMessageSource
-) : MessageChainBuilder() {
-    var kind: OfflineMessageSource.Kind = source.kind
-    var fromUin: Long = source.fromId
-    var targetUin: Long = source.targetId
-    var id: Int = source.id
-    var time: Int = source.time
-    var internalId: Int = source.internalId
-    var originalMessage: MessageChain = source.originalMessage
+    internal abstract var id: Int
+    internal abstract var time: Int
+    internal abstract var internalId: Int
 
-    fun from(sender: Contact): MessageSourceBuilder {
-        fromUin = if (sender is Group) {
-            Group.calculateGroupUinByGroupCode(sender.id)
-        } else sender.id
+    @PublishedApi
+    internal val originalMessages: MessageChainBuilder = MessageChainBuilder()
+
+    fun time(from: MessageSource): MessageSourceBuilder = apply { this.time = from.time }
+    val now: Int get() = currentTimeSeconds.toInt()
+    fun time(value: Int) = apply { this.time = value }
+
+    fun internalId(from: MessageSource): MessageSourceBuilder = apply { this.internalId = from.internalId }
+    fun internalId(value: Int): MessageSourceBuilder = apply { this.internalId = value }
+
+    fun id(from: MessageSource): MessageSourceBuilder = apply { this.id = from.id }
+    fun id(value: Int): MessageSourceBuilder = apply { this.id = value }
+
+
+    /**
+     * 从另一个 [MessageSource] 复制 [id], [time], [internalId].
+     * 这三个数据决定官方客户端能 "定位" 到的原消息
+     */
+    fun metadata(from: MessageSource): MessageSourceBuilder = apply {
+        id(from)
+        internalId(from)
+        time(from)
+    }
+
+    /**
+     * 从另一个 [MessageSource] 复制所有信息, 包括消息内容. 不会清空已有消息.
+     */
+    fun allFrom(source: MessageSource): MessageSourceBuilder {
+        this.kind = determineKind(source)
+        this.id = source.id
+        this.time = source.time
+        this.fromUin = source.fromId
+        this.targetUin = source.targetId
+        this.internalId = source.internalId
+        this.originalMessages.addAll(source.originalMessage)
         return this
     }
 
-    fun target(target: Contact): MessageSourceBuilder {
+
+    /**
+     * 从另一个 [MessageSource] 复制 [消息内容][MessageSource.originalMessage]. 不会清空已有消息.
+     */
+    fun messagesFrom(source: MessageSource): MessageSourceBuilder = apply {
+        this.originalMessages.addAll(source.originalMessage)
+    }
+
+    fun messages(messages: Iterable<Message>): MessageSourceBuilder = apply {
+        this.originalMessages.addAll(messages)
+    }
+
+    fun messages(vararg message: Message): MessageSourceBuilder = apply {
+        for (it in message) {
+            this.originalMessages.add(it)
+        }
+    }
+
+    @JvmSynthetic
+    inline fun messages(block: MessageChainBuilder.() -> Unit): MessageSourceBuilder = apply {
+        this.originalMessages.apply(block)
+    }
+
+    fun clearMessages(): MessageSourceBuilder = apply { this.originalMessages.clear() }
+
+    /**
+     * 设置 [发送人][this] 和 [发送目标][target], 并自动判断 [kind]
+     */
+    @JvmSynthetic
+    abstract infix fun ContactOrBot.sendTo(target: ContactOrBot): MessageSourceBuilder
+
+    fun setSenderAndTarget(sender: ContactOrBot, target: ContactOrBot) = sender sendTo target
+}
+
+
+//////////////////
+//// INTERNAL ////
+//////////////////
+
+
+internal class MessageSourceBuilderImpl : MessageSourceBuilder() {
+    override var kind: OfflineMessageSource.Kind? = null
+    override var fromUin: Long = 0
+    override var targetUin: Long = 0
+
+    override var id: Int = 0
+    override var time: Int = currentTimeSeconds.toInt()
+    override var internalId: Int = 0
+
+    @JvmSynthetic
+    override fun ContactOrBot.sendTo(target: ContactOrBot): MessageSourceBuilder {
+        fromUin = if (this is Group) {
+            Group.calculateGroupUinByGroupCode(this.id)
+        } else this.id
+
         targetUin = if (target is Group) {
             Group.calculateGroupUinByGroupCode(target.id)
         } else target.id
-        return this
-    }
 
-    fun
+        check(this != target) { "sender and target mustn't be the same" }
+
+        kind = when {
+            this is Group || target is Group -> OfflineMessageSource.Kind.GROUP
+            this is Member || target is Member -> OfflineMessageSource.Kind.TEMP
+            this is Bot && target is Friend -> OfflineMessageSource.Kind.FRIEND
+            this is Friend && target is Bot -> OfflineMessageSource.Kind.FRIEND
+            else -> throw IllegalArgumentException("Cannot determine source kind for sender $this and target $target")
+        }
+        return this@MessageSourceBuilderImpl
+    }
+}
+
+
+@JvmSynthetic
+internal fun MessageSource.toMutableOffline(): MutableOfflineMessageSourceByOnline =
+    MutableOfflineMessageSourceByOnline(this)
+
+internal class MutableOfflineMessageSourceByOnline(
+    origin: MessageSource
+) : OfflineMessageSource(), MessageSourceAmender {
+    override var kind: Kind = determineKind(origin)
+    override var fromUin: Long
+        get() = fromId
+        set(value) {
+            fromId = value
+        }
+    override var targetUin: Long
+        get() = targetId
+        set(value) {
+            targetId = value
+        }
+    override var bot: Bot = origin.bot
+    override var id: Int = origin.id
+    override var internalId: Int = origin.internalId
+    override var time: Int = origin.time
+    override var fromId: Long = origin.fromId
+    override var targetId: Long = origin.targetId
+    override var originalMessage: MessageChain = origin.originalMessage
+}
+
+private fun determineKind(source: MessageSource): OfflineMessageSource.Kind {
+    return when {
+        source.isAboutGroup() -> OfflineMessageSource.Kind.GROUP
+        source.isAboutFriend() -> OfflineMessageSource.Kind.FRIEND
+        source.isAboutTemp() -> OfflineMessageSource.Kind.TEMP
+        else -> error("stub")
+    }
 }
\ No newline at end of file