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