Browse Source

Rearrange implementations for MessageSource, implement recall state check

Him188 5 years ago
parent
commit
db8cb84c99

+ 6 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt

@@ -291,11 +291,14 @@ internal abstract class QQAndroidBotBase constructor(
 
     @Suppress("RemoveExplicitTypeArguments") // false positive
     override suspend fun recall(source: MessageSource) {
-        // println(source._miraiContentToString())
-
-        check(source is MessageSourceImpl)
+        check(source is MessageSourceInternal)
         source.ensureSequenceIdAvailable()
 
+        @Suppress("BooleanLiteralArgument") // false positive
+        check(!source.isRecalledOrPlanned.value && source.isRecalledOrPlanned.compareAndSet(false, true)) {
+            "$source had already been recalled."
+        }
+
         val response: PbMessageSvc.PbMsgWithDraw.Response = when (source) {
             is MessageSourceToGroupImpl,
             is MessageSourceFromGroupImpl

+ 0 - 506
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt

@@ -1,506 +0,0 @@
-/*
- * Copyright 2020 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/master/LICENSE
- */
-
-@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
-
-package net.mamoe.mirai.qqandroid.message
-
-import kotlinx.atomicfu.AtomicBoolean
-import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.Member
-import net.mamoe.mirai.contact.QQ
-import net.mamoe.mirai.event.subscribingGetAsync
-import net.mamoe.mirai.message.data.MessageChain
-import net.mamoe.mirai.message.data.MessageSource
-import net.mamoe.mirai.message.data.OfflineMessageSource
-import net.mamoe.mirai.message.data.OnlineMessageSource
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
-import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
-import net.mamoe.mirai.qqandroid.utils._miraiContentToString
-import net.mamoe.mirai.qqandroid.utils.io.serialization.loadAs
-import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
-import net.mamoe.mirai.utils.MiraiExperimentalAPI
-
-
-internal interface MessageSourceImpl {
-    val sequenceId: Int
-
-    var isRecalledOrPlanned: Boolean // // TODO: 2020/4/7 实现 isRecalledOrPlanned
-
-    fun toJceData(): ImMsgBody.SourceMsg
-}
-
-internal suspend inline fun MessageSource.ensureSequenceIdAvailable() {
-    if (this is MessageSourceToGroupImpl) {
-        this.ensureSequenceIdAvailable()
-    }
-}
-
-internal class MessageSourceFromFriendImpl(
-    override val bot: Bot,
-    val msg: MsgComm.Msg
-) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceImpl {
-    override val sequenceId: Int get() = msg.msgHead.msgSeq
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    override val id: Int get() = msg.msgBody.richText.attr!!.random
-    override val time: Int get() = msg.msgHead.msgTime
-    override val originalMessage: MessageChain by lazy {
-        msg.toMessageChain(
-            bot,
-            groupIdOrZero = 0,
-            onlineSource = false
-        )
-    }
-    override val sender: QQ get() = bot.getFriend(msg.msgHead.fromUin)
-
-    private val elems by lazy {
-        msg.msgBody.richText.elems.toMutableList().also {
-            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
-        }
-    }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(msg.msgHead.msgSeq),
-            senderUin = msg.msgHead.fromUin,
-            toUin = msg.msgHead.toUin,
-            flag = 1,
-            elems = msg.msgBody.richText.elems,
-            type = 0,
-            time = msg.msgHead.msgTime,
-            pbReserve = SourceMsg.ResvAttr(
-                origUids = id.toLong() and 0xFFFF_FFFF
-            ).toByteArray(SourceMsg.ResvAttr.serializer()),
-            srcMsg = MsgComm.Msg(
-                msgHead = MsgComm.MsgHead(
-                    fromUin = msg.msgHead.fromUin, // qq
-                    toUin = msg.msgHead.toUin, // group
-                    msgType = msg.msgHead.msgType, // 82?
-                    c2cCmd = msg.msgHead.c2cCmd,
-                    msgSeq = msg.msgHead.msgSeq,
-                    msgTime = msg.msgHead.msgTime,
-                    msgUid = id.toLong() and 0xFFFF_FFFF, // ok
-                    // groupInfo = MsgComm.GroupInfo(groupCode = msg.msgHead.groupInfo.groupCode),
-                    isSrcMsg = true
-                ),
-                msgBody = ImMsgBody.MsgBody(
-                    richText = ImMsgBody.RichText(
-                        elems = elems
-                    )
-                )
-            ).toByteArray(MsgComm.Msg.serializer())
-        )
-    }
-}
-
-internal class MessageSourceFromTempImpl(
-    override val bot: Bot,
-    private val msg: MsgComm.Msg
-) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceImpl {
-    override val sequenceId: Int get() = msg.msgHead.msgSeq
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    override val id: Int get() = msg.msgBody.richText.attr!!.random
-    override val time: Int get() = msg.msgHead.msgTime
-    override val originalMessage: MessageChain by lazy {
-        msg.toMessageChain(
-            bot,
-            groupIdOrZero = 0,
-            onlineSource = false
-        )
-    }
-    override val sender: Member
-        get() = with(msg.msgHead) {
-            bot.getGroup(c2cTmpMsgHead!!.groupUin)[fromUin]
-        }
-
-    private val elems by lazy {
-        msg.msgBody.richText.elems.toMutableList().also {
-            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
-        }
-    }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(msg.msgHead.msgSeq),
-            senderUin = msg.msgHead.fromUin,
-            toUin = msg.msgHead.toUin,
-            flag = 1,
-            elems = msg.msgBody.richText.elems,
-            type = 0,
-            time = msg.msgHead.msgTime,
-            pbReserve = SourceMsg.ResvAttr(
-                origUids = id.toLong() and 0xFFFF_FFFF
-            ).toByteArray(SourceMsg.ResvAttr.serializer()),
-            srcMsg = MsgComm.Msg(
-                msgHead = MsgComm.MsgHead(
-                    fromUin = msg.msgHead.fromUin, // qq
-                    toUin = msg.msgHead.toUin, // group
-                    msgType = msg.msgHead.msgType, // 82?
-                    c2cCmd = msg.msgHead.c2cCmd,
-                    msgSeq = msg.msgHead.msgSeq,
-                    msgTime = msg.msgHead.msgTime,
-                    msgUid = id.toLong() and 0xFFFF_FFFF, // ok
-                    // groupInfo = MsgComm.GroupInfo(groupCode = msg.msgHead.groupInfo.groupCode),
-                    isSrcMsg = true
-                ),
-                msgBody = ImMsgBody.MsgBody(
-                    richText = ImMsgBody.RichText(
-                        elems = elems
-                    )
-                )
-            ).toByteArray(MsgComm.Msg.serializer())
-        )
-    }
-}
-
-internal class MessageSourceFromGroupImpl(
-    override val bot: Bot,
-    private val msg: MsgComm.Msg
-) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceImpl {
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    override val sequenceId: Int get() = msg.msgHead.msgSeq
-    override val id: Int get() = msg.msgBody.richText.attr!!.random
-    override val time: Int get() = msg.msgHead.msgTime
-    override val originalMessage: MessageChain by lazy {
-        msg.toMessageChain(
-            bot,
-            groupIdOrZero = group.id,
-            onlineSource = false
-        )
-    }
-    override val sender: Member
-        get() = bot.getGroup(
-            msg.msgHead.groupInfo?.groupCode
-                ?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
-        ).getOrNull(msg.msgHead.fromUin)
-            ?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
-
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(msg.msgHead.msgSeq),
-            senderUin = msg.msgHead.fromUin,
-            toUin = 0,
-            flag = 1,
-            elems = msg.msgBody.richText.elems,
-            type = 0,
-            time = msg.msgHead.msgTime,
-            pbReserve = EMPTY_BYTE_ARRAY,
-            srcMsg = EMPTY_BYTE_ARRAY
-        )
-    }
-}
-
-internal class OfflineMessageSourceImplByMsg(
-    // from other sources' originalMessage
-    val delegate: MsgComm.Msg,
-    override val bot: Bot
-) : OfflineMessageSource(), MessageSourceImpl {
-    override val kind: Kind = if (delegate.msgHead.groupInfo != null) Kind.GROUP else Kind.FRIEND
-    override val id: Int
-        get() = delegate.msgHead.msgUid.toInt()
-    override val time: Int
-        get() = delegate.msgHead.msgTime
-    override val fromId: Long
-        get() = delegate.msgHead.fromUin
-    override val targetId: Long
-        get() = delegate.msgHead.groupInfo?.groupCode ?: delegate.msgHead.toUin
-    override val originalMessage: MessageChain by lazy {
-        delegate.toMessageChain(bot,
-            groupIdOrZero = delegate.msgHead.groupInfo?.groupCode ?: 0,
-            onlineSource = false,
-            isTemp = delegate.msgHead.c2cTmpMsgHead != null
-        )
-    }
-    override val sequenceId: Int
-        get() = delegate.msgHead.msgSeq
-
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(delegate.msgHead.msgSeq),
-            senderUin = delegate.msgHead.fromUin,
-            toUin = 0,
-            flag = 1,
-            elems = delegate.msgBody.richText.elems,
-            type = 0,
-            time = delegate.msgHead.msgTime,
-            pbReserve = EMPTY_BYTE_ARRAY,
-            srcMsg = EMPTY_BYTE_ARRAY
-        )
-    }
-}
-
-internal class OfflineMessageSourceImplBySourceMsg(
-    // from others' quotation
-    val delegate: ImMsgBody.SourceMsg,
-    override val bot: Bot,
-    groupIdOrZero: Long
-) : OfflineMessageSource(), MessageSourceImpl {
-    override val kind: Kind get() = if (delegate.srcMsg == null) Kind.GROUP else Kind.FRIEND
-
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    override val sequenceId: Int
-        get() = delegate.origSeqs?.first() ?: error("cannot find sequenceId")
-    override val time: Int get() = delegate.time
-    override val originalMessage: MessageChain by lazy { delegate.toMessageChain(bot, groupIdOrZero) }
-    /*
-    override val id: Long
-        get() = (delegate.origSeqs?.firstOrNull()
-            ?: error("cannot find sequenceId from ImMsgBody.SourceMsg")).toLong().shl(32) or
-                delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.and(0xFFFFFFFF)
-    */
-
-    override val id: Int
-        get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids?.toInt()
-            ?: 0
-
-    // override val sourceMessage: MessageChain get() = delegate.toMessageChain()
-    override val fromId: Long get() = delegate.senderUin
-    override val targetId: Long by lazy {
-        when {
-            groupIdOrZero != 0L -> groupIdOrZero
-            delegate.toUin != 0L -> delegate.toUin
-            delegate.srcMsg != null -> delegate.srcMsg.loadAs(MsgComm.Msg.serializer()).msgHead.toUin
-            else -> 0/*error("cannot find targetId. delegate=${delegate._miraiContentToString()}, delegate.srcMsg=${
-            kotlin.runCatching { delegate.srcMsg?.loadAs(MsgComm.Msg.serializer())?._miraiContentToString() }
-                .fold(
-                    onFailure = { "<error: ${it.message}>" },
-                    onSuccess = { it }
-                )
-            }"
-            )*/
-        }
-    }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return delegate
-    }
-}
-
-internal class MessageSourceToFriendImpl(
-    override val sequenceId: Int,
-    override val id: Int,
-    override val time: Int,
-    override val originalMessage: MessageChain,
-    override val sender: Bot,
-    override val target: QQ
-) : OnlineMessageSource.Outgoing.ToFriend(), MessageSourceImpl {
-    override val bot: Bot
-        get() = sender
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    private val elems by lazy {
-        originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
-    }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        val messageUid: Long = sequenceId.toLong().shl(32) or id.toLong().and(0xffFFffFF)
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(sequenceId),
-            senderUin = fromId,
-            toUin = targetId,
-            flag = 1,
-            elems = elems,
-            type = 0,
-            time = time,
-            pbReserve = SourceMsg.ResvAttr(
-                origUids = messageUid
-            ).toByteArray(SourceMsg.ResvAttr.serializer()),
-            srcMsg = MsgComm.Msg(
-                msgHead = MsgComm.MsgHead(
-                    fromUin = fromId, // qq
-                    toUin = targetId, // group
-                    msgType = 9, // 82?
-                    c2cCmd = 11,
-                    msgSeq = sequenceId,
-                    msgTime = time,
-                    msgUid = messageUid, // ok
-                    // groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
-                    isSrcMsg = true
-                ),
-                msgBody = ImMsgBody.MsgBody(
-                    richText = ImMsgBody.RichText(
-                        elems = elems.toMutableList().also {
-                            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
-                        }
-                    )
-                )
-            ).toByteArray(MsgComm.Msg.serializer())
-        )
-    }
-}
-
-internal class MessageSourceToTempImpl(
-    override val sequenceId: Int,
-    override val id: Int,
-    override val time: Int,
-    override val originalMessage: MessageChain,
-    override val sender: Bot,
-    override val target: Member
-) : OnlineMessageSource.Outgoing.ToTemp(), MessageSourceImpl {
-    override val bot: Bot
-        get() = sender
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    private val elems by lazy {
-        originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
-    }
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        val messageUid: Long = sequenceId.toLong().shl(32) or id.toLong().and(0xffFFffFF)
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(sequenceId),
-            senderUin = fromId,
-            toUin = targetId,
-            flag = 1,
-            elems = elems,
-            type = 0,
-            time = time,
-            pbReserve = SourceMsg.ResvAttr(
-                origUids = messageUid
-            ).toByteArray(SourceMsg.ResvAttr.serializer()),
-            srcMsg = MsgComm.Msg(
-                msgHead = MsgComm.MsgHead(
-                    fromUin = fromId, // qq
-                    toUin = targetId, // group
-                    msgType = 9, // 82?
-                    c2cCmd = 11,
-                    msgSeq = sequenceId,
-                    msgTime = time,
-                    msgUid = messageUid, // ok
-                    // groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
-                    isSrcMsg = true
-                ),
-                msgBody = ImMsgBody.MsgBody(
-                    richText = ImMsgBody.RichText(
-                        elems = elems.toMutableList().also {
-                            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
-                        }
-                    )
-                )
-            ).toByteArray(MsgComm.Msg.serializer())
-        )
-    }
-}
-
-internal class MessageSourceToGroupImpl(
-    override val id: Int,
-    override val time: Int,
-    override val originalMessage: MessageChain,
-    override val sender: Bot,
-    override val target: Group
-) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceImpl {
-    override val bot: Bot
-        get() = sender
-    private val isRecalled: AtomicBoolean = atomic(false)
-    override var isRecalledOrPlanned: Boolean
-        get() = isRecalled.value
-        set(value) {
-            isRecalled.value = value
-        }
-    private val elems by lazy {
-        originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
-    }
-    private lateinit var sequenceIdDeferred: Deferred<Int>
-    override val sequenceId: Int get() = sequenceIdDeferred.getCompleted()
-
-    @OptIn(MiraiExperimentalAPI::class)
-    internal fun startWaitingSequenceId(coroutineScope: CoroutineScope) {
-        sequenceIdDeferred =
-            coroutineScope.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int>(
-                timeoutMillis = 3000
-            ) {
-                if (it.messageRandom == [email protected]) {
-                    it.sequenceId
-                } else null
-            }
-    }
-
-    suspend fun ensureSequenceIdAvailable() {
-        sequenceIdDeferred.join()
-    }
-
-
-    override fun toJceData(): ImMsgBody.SourceMsg {
-        return ImMsgBody.SourceMsg(
-            origSeqs = listOf(sequenceId),
-            senderUin = fromId,
-            toUin = Group.calculateGroupUinByGroupCode(targetId),
-            flag = 1,
-            elems = elems,
-            type = 0,
-            time = time,
-            pbReserve = SourceMsg.ResvAttr(
-                origUids = id.toLong() and 0xffFFffFF // id is actually messageRandom
-            ).toByteArray(SourceMsg.ResvAttr.serializer()),
-            srcMsg = MsgComm.Msg(
-                msgHead = MsgComm.MsgHead(
-                    fromUin = fromId, // qq
-                    toUin = Group.calculateGroupUinByGroupCode(targetId), // group
-                    msgType = 82, // 82?
-                    c2cCmd = 1,
-                    msgSeq = sequenceId,
-                    msgTime = time,
-                    msgUid = id.toLong() and 0xffFFffFF, // ok
-                    groupInfo = MsgComm.GroupInfo(groupCode = targetId),
-                    isSrcMsg = true
-                ),
-                msgBody = ImMsgBody.MsgBody(
-                    richText = ImMsgBody.RichText(
-                        elems = elems.toMutableList().also {
-                            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
-                        }
-                    )
-                )
-            ).toByteArray(MsgComm.Msg.serializer())
-        )
-    }
-}

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt

@@ -40,7 +40,7 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
 
     if (this.anyIsInstance<QuoteReply>()) {
         when (val source = this[QuoteReply].source) {
-            is MessageSourceImpl -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
+            is MessageSourceInternal -> elements.add(ImMsgBody.Elem(srcMsg = source.toJceData()))
             else -> error("unsupported MessageSource implementation: ${source::class.simpleName}. Don't implement your own MessageSource.")
         }
     }

+ 141 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/incomingSourceImpl.kt

@@ -0,0 +1,141 @@
+/*
+ * Copyright 2020 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/master/LICENSE
+ */
+
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+package net.mamoe.mirai.qqandroid.message
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Member
+import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.message.data.OnlineMessageSource
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.qqandroid.utils._miraiContentToString
+import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
+
+internal interface MessageSourceInternal {
+    val sequenceId: Int
+
+    val isRecalledOrPlanned: MiraiAtomicBoolean
+
+    fun toJceData(): ImMsgBody.SourceMsg
+}
+
+internal suspend inline fun MessageSource.ensureSequenceIdAvailable() {
+    if (this is MessageSourceToGroupImpl) {
+        this.ensureSequenceIdAvailable()
+    }
+}
+
+internal class MessageSourceFromFriendImpl(
+    override val bot: Bot,
+    val msg: MsgComm.Msg
+) : OnlineMessageSource.Incoming.FromFriend(), MessageSourceInternal {
+    override val sequenceId: Int get() = msg.msgHead.msgSeq
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    override val id: 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: QQ get() = bot.getFriend(msg.msgHead.fromUin)
+
+    private val jceData by lazy { msg.toJceDataFriendOrTemp(id) }
+
+    override fun toJceData(): ImMsgBody.SourceMsg = jceData
+}
+
+private fun MsgComm.Msg.toJceDataFriendOrTemp(id: Int): ImMsgBody.SourceMsg {
+    val elements = msgBody.richText.elems.toMutableList().also {
+        if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
+    }
+    return ImMsgBody.SourceMsg(
+        origSeqs = listOf(this.msgHead.msgSeq),
+        senderUin = this.msgHead.fromUin,
+        toUin = this.msgHead.toUin,
+        flag = 1,
+        elems = this.msgBody.richText.elems,
+        type = 0,
+        time = this.msgHead.msgTime,
+        pbReserve = SourceMsg.ResvAttr(
+            origUids = id.toLong() and 0xFFFF_FFFF
+        ).toByteArray(SourceMsg.ResvAttr.serializer()),
+        srcMsg = MsgComm.Msg(
+            msgHead = MsgComm.MsgHead(
+                fromUin = this.msgHead.fromUin, // qq
+                toUin = this.msgHead.toUin, // group
+                msgType = this.msgHead.msgType, // 82?
+                c2cCmd = this.msgHead.c2cCmd,
+                msgSeq = this.msgHead.msgSeq,
+                msgTime = this.msgHead.msgTime,
+                msgUid = id.toLong() and 0xFFFF_FFFF, // ok
+                // groupInfo = MsgComm.GroupInfo(groupCode = this.msgHead.groupInfo.groupCode),
+                isSrcMsg = true
+            ),
+            msgBody = ImMsgBody.MsgBody(
+                richText = ImMsgBody.RichText(
+                    elems = elements
+                )
+            )
+        ).toByteArray(MsgComm.Msg.serializer())
+    )
+}
+
+internal class MessageSourceFromTempImpl(
+    override val bot: Bot,
+    private val msg: MsgComm.Msg
+) : OnlineMessageSource.Incoming.FromTemp(), MessageSourceInternal {
+    override val sequenceId: Int get() = msg.msgHead.msgSeq
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    override val id: 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: Member get() = with(msg.msgHead) { bot.getGroup(c2cTmpMsgHead!!.groupUin)[fromUin] }
+
+    private val jceData by lazy { msg.toJceDataFriendOrTemp(id) }
+    override fun toJceData(): ImMsgBody.SourceMsg = jceData
+}
+
+internal data class MessageSourceFromGroupImpl(
+    override val bot: Bot,
+    private val msg: MsgComm.Msg
+) : OnlineMessageSource.Incoming.FromGroup(), MessageSourceInternal {
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    override val sequenceId: Int get() = msg.msgHead.msgSeq
+    override val id: Int get() = msg.msgBody.richText.attr!!.random
+    override val time: Int get() = msg.msgHead.msgTime
+    override val originalMessage: MessageChain by lazy {
+        msg.toMessageChain(bot, groupIdOrZero = group.id, onlineSource = false)
+    }
+    override val sender: Member
+        get() = bot.getGroup(
+            msg.msgHead.groupInfo?.groupCode
+                ?: error("cannot find groupCode for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
+        ).getOrNull(msg.msgHead.fromUin)
+            ?: error("cannot find member for MessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
+
+
+    override fun toJceData(): ImMsgBody.SourceMsg {
+        return ImMsgBody.SourceMsg(
+            origSeqs = listOf(msg.msgHead.msgSeq),
+            senderUin = msg.msgHead.fromUin,
+            toUin = 0,
+            flag = 1,
+            elems = msg.msgBody.richText.elems,
+            type = 0,
+            time = msg.msgHead.msgTime,
+            pbReserve = EMPTY_BYTE_ARRAY,
+            srcMsg = EMPTY_BYTE_ARRAY
+        )
+    }
+}

+ 110 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/offlineSourceImpl.kt

@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 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/master/LICENSE
+ */
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+package net.mamoe.mirai.qqandroid.message
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.OfflineMessageSource
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.qqandroid.utils.io.serialization.loadAs
+
+
+internal class OfflineMessageSourceImplByMsg(
+    // from other sources' originalMessage
+    val delegate: MsgComm.Msg,
+    override val bot: Bot
+) : OfflineMessageSource(), MessageSourceInternal {
+    override val kind: Kind = if (delegate.msgHead.groupInfo != null) Kind.GROUP else Kind.FRIEND
+    override val id: Int
+        get() = delegate.msgHead.msgUid.toInt()
+    override val time: Int
+        get() = delegate.msgHead.msgTime
+    override val fromId: Long
+        get() = delegate.msgHead.fromUin
+    override val targetId: Long
+        get() = delegate.msgHead.groupInfo?.groupCode ?: delegate.msgHead.toUin
+    override val originalMessage: MessageChain by lazy {
+        delegate.toMessageChain(bot,
+            groupIdOrZero = delegate.msgHead.groupInfo?.groupCode ?: 0,
+            onlineSource = false,
+            isTemp = delegate.msgHead.c2cTmpMsgHead != null
+        )
+    }
+    override val sequenceId: Int
+        get() = delegate.msgHead.msgSeq
+
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+
+    override fun toJceData(): ImMsgBody.SourceMsg {
+        return ImMsgBody.SourceMsg(
+            origSeqs = listOf(delegate.msgHead.msgSeq),
+            senderUin = delegate.msgHead.fromUin,
+            toUin = 0,
+            flag = 1,
+            elems = delegate.msgBody.richText.elems,
+            type = 0,
+            time = delegate.msgHead.msgTime,
+            pbReserve = EMPTY_BYTE_ARRAY,
+            srcMsg = EMPTY_BYTE_ARRAY
+        )
+    }
+}
+
+internal class OfflineMessageSourceImplBySourceMsg(
+    // from others' quotation
+    val delegate: ImMsgBody.SourceMsg,
+    override val bot: Bot,
+    groupIdOrZero: Long
+) : OfflineMessageSource(), MessageSourceInternal {
+    override val kind: Kind get() = if (delegate.srcMsg == null) Kind.GROUP else Kind.FRIEND
+
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    override val sequenceId: Int
+        get() = delegate.origSeqs?.first() ?: error("cannot find sequenceId")
+    override val time: Int get() = delegate.time
+    override val originalMessage: MessageChain by lazy { delegate.toMessageChain(bot, groupIdOrZero) }
+    /*
+    override val id: Long
+        get() = (delegate.origSeqs?.firstOrNull()
+            ?: error("cannot find sequenceId from ImMsgBody.SourceMsg")).toLong().shl(32) or
+                delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.and(0xFFFFFFFF)
+    */
+
+    override val id: Int
+        get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids?.toInt()
+            ?: 0
+
+    // override val sourceMessage: MessageChain get() = delegate.toMessageChain()
+    override val fromId: Long get() = delegate.senderUin
+    override val targetId: Long by lazy {
+        when {
+            groupIdOrZero != 0L -> groupIdOrZero
+            delegate.toUin != 0L -> delegate.toUin
+            delegate.srcMsg != null -> delegate.srcMsg.loadAs(MsgComm.Msg.serializer()).msgHead.toUin
+            else -> 0/*error("cannot find targetId. delegate=${delegate._miraiContentToString()}, delegate.srcMsg=${
+            kotlin.runCatching { delegate.srcMsg?.loadAs(MsgComm.Msg.serializer())?._miraiContentToString() }
+                .fold(
+                    onFailure = { "<error: ${it.message}>" },
+                    onSuccess = { it }
+                )
+            }"
+            )*/
+        }
+    }
+
+    override fun toJceData(): ImMsgBody.SourceMsg {
+        return delegate
+    }
+}

+ 169 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/outgoingSourceImpl.kt

@@ -0,0 +1,169 @@
+/*
+ * Copyright 2020 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/master/LICENSE
+ */
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
+
+package net.mamoe.mirai.qqandroid.message
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.Member
+import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
+import net.mamoe.mirai.event.subscribingGetAsync
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.message.data.OnlineMessageSource
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
+import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+
+
+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 id.toLong().and(0xffFFffFF)
+    return ImMsgBody.SourceMsg(
+        origSeqs = listOf(sequenceId),
+        senderUin = fromId,
+        toUin = targetId,
+        flag = 1,
+        elems = elements,
+        type = 0,
+        time = time,
+        pbReserve = SourceMsg.ResvAttr(
+            origUids = messageUid
+        ).toByteArray(SourceMsg.ResvAttr.serializer()),
+        srcMsg = MsgComm.Msg(
+            msgHead = MsgComm.MsgHead(
+                fromUin = fromId, // qq
+                toUin = targetId, // group
+                msgType = 9, // 82?
+                c2cCmd = 11,
+                msgSeq = sequenceId,
+                msgTime = time,
+                msgUid = messageUid, // ok
+                // groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
+                isSrcMsg = true
+            ),
+            msgBody = ImMsgBody.MsgBody(
+                richText = ImMsgBody.RichText(
+                    elems = elements.toMutableList().also {
+                        if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
+                    }
+                )
+            )
+        ).toByteArray(MsgComm.Msg.serializer())
+    )
+}
+
+internal class MessageSourceToFriendImpl(
+    override val sequenceId: Int,
+    override val id: Int,
+    override val time: Int,
+    override val originalMessage: MessageChain,
+    override val sender: Bot,
+    override val target: QQ
+) : OnlineMessageSource.Outgoing.ToFriend(), MessageSourceInternal {
+    override val bot: Bot
+        get() = sender
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    private val jceData by lazy { toJceDataImpl() }
+    override fun toJceData(): ImMsgBody.SourceMsg = jceData
+}
+
+internal class MessageSourceToTempImpl(
+    override val sequenceId: Int,
+    override val id: Int,
+    override val time: Int,
+    override val originalMessage: MessageChain,
+    override val sender: Bot,
+    override val target: Member
+) : OnlineMessageSource.Outgoing.ToTemp(), MessageSourceInternal {
+    override val bot: Bot
+        get() = sender
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    private val jceData by lazy { toJceDataImpl() }
+    override fun toJceData(): ImMsgBody.SourceMsg = jceData
+}
+
+internal class MessageSourceToGroupImpl(
+    override val id: Int,
+    override val time: Int,
+    override val originalMessage: MessageChain,
+    override val sender: Bot,
+    override val target: Group
+) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceInternal {
+    override val bot: Bot
+        get() = sender
+    override var isRecalledOrPlanned: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
+    private lateinit var sequenceIdDeferred: Deferred<Int>
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    override val sequenceId: Int
+        get() = sequenceIdDeferred.getCompleted()
+
+    @OptIn(MiraiExperimentalAPI::class)
+    internal fun startWaitingSequenceId(coroutineScope: CoroutineScope) {
+        sequenceIdDeferred =
+            coroutineScope.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int>(
+                timeoutMillis = 3000
+            ) {
+                if (it.messageRandom == [email protected]) {
+                    it.sequenceId
+                } else null
+            }
+    }
+
+    suspend fun ensureSequenceIdAvailable() = sequenceIdDeferred.join()
+
+    private val jceData by lazy {
+        val elements = originalMessage.toRichTextElems(forGroup = false, withGeneralFlags = true)
+        ImMsgBody.SourceMsg(
+            origSeqs = listOf(sequenceId),
+            senderUin = fromId,
+            toUin = Group.calculateGroupUinByGroupCode(targetId),
+            flag = 1,
+            elems = elements,
+            type = 0,
+            time = time,
+            pbReserve = SourceMsg.ResvAttr(
+                origUids = id.toLong() and 0xffFFffFF // id is actually messageRandom
+            ).toByteArray(SourceMsg.ResvAttr.serializer()),
+            srcMsg = MsgComm.Msg(
+                msgHead = MsgComm.MsgHead(
+                    fromUin = fromId, // qq
+                    toUin = Group.calculateGroupUinByGroupCode(targetId), // group
+                    msgType = 82, // 82?
+                    c2cCmd = 1,
+                    msgSeq = sequenceId,
+                    msgTime = time,
+                    msgUid = id.toLong() and 0xffFFffFF, // ok
+                    groupInfo = MsgComm.GroupInfo(groupCode = targetId),
+                    isSrcMsg = true
+                ),
+                msgBody = ImMsgBody.MsgBody(
+                    richText = ImMsgBody.RichText(
+                        elems = elements.toMutableList().also {
+                            if (it.last().elemFlags2 == null) it.add(ImMsgBody.Elem(elemFlags2 = ImMsgBody.ElemFlags2()))
+                        }
+                    )
+                )
+            ).toByteArray(MsgComm.Msg.serializer())
+        )
+    }
+
+    override fun toJceData(): ImMsgBody.SourceMsg = jceData
+}

+ 1 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt

@@ -157,6 +157,7 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI(
      * @param source 消息源. 可从 [MessageReceipt.source] 获得, 或从消息事件中的 [MessageChain] 获得.
      *
      * @throws PermissionDeniedException 当 [Bot] 无权限操作时
+     * @throws IllegalStateException 当这条消息已经被撤回时 (仅同步主动操作)
      *
      * @see Bot.recall (扩展函数) 接受参数 [MessageChain]
      * @see MessageSource.recall