123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- /*
- * 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
- */
- package net.mamoe.mirai.internal.contact
- import kotlinx.coroutines.CompletableDeferred
- import kotlinx.coroutines.Deferred
- import kotlinx.coroutines.async
- import net.mamoe.mirai.contact.*
- import net.mamoe.mirai.event.nextEventOrNull
- import net.mamoe.mirai.internal.asQQAndroidBot
- import net.mamoe.mirai.internal.getMiraiImpl
- import net.mamoe.mirai.internal.message.*
- import net.mamoe.mirai.internal.network.Packet
- import net.mamoe.mirai.internal.network.QQAndroidClient
- import net.mamoe.mirai.internal.network.components.ClockHolder.Companion.clock
- import net.mamoe.mirai.internal.network.components.MessageSvcSyncer
- import net.mamoe.mirai.internal.network.handler.logger
- import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor.SendGroupMessageReceipt
- import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor.SendPrivateMessageReceipt
- import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
- import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
- import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
- import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
- import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.*
- import net.mamoe.mirai.internal.utils.ImagePatcher
- import net.mamoe.mirai.message.MessageReceipt
- import net.mamoe.mirai.message.data.*
- import net.mamoe.mirai.utils.castOrNull
- import net.mamoe.mirai.utils.currentTimeSeconds
- /**
- * 处理 mirai 消息系统 `Message` 到协议数据结构的转换.
- *
- * 外部调用 [sendMessageImpl]
- */
- internal abstract class SendMessageHandler<C : Contact> {
- abstract val contact: C
- abstract val senderName: String
- val messageSourceKind: MessageSourceKind
- get() {
- return when (contact) {
- is Group -> MessageSourceKind.GROUP
- is Friend -> MessageSourceKind.FRIEND
- is Member -> MessageSourceKind.TEMP
- is Stranger -> MessageSourceKind.STRANGER
- else -> error("Unsupported contact: $contact")
- }
- }
- val bot get() = contact.bot.asQQAndroidBot()
- val targetUserUin: Long? get() = contact.castOrNull<User>()?.uin
- val targetGroupUin: Long? get() = contact.castOrNull<Group>()?.uin
- val targetGroupCode: Long? get() = contact.castOrNull<Group>()?.groupCode
- val targetOtherClientBotUin: Long? get() = contact.castOrNull<OtherClient>()?.bot?.id
- val targetUin: Long get() = targetGroupUin ?: targetOtherClientBotUin ?: contact.id
- val groupInfo: MsgComm.GroupInfo?
- get() = if (isToGroup) MsgComm.GroupInfo(
- groupCode = targetGroupCode!!,
- groupCard = senderName // Cinnamon
- ) else null
- // For ForwardMessage display
- val ForwardMessage.INode.groupInfo: MsgComm.GroupInfo
- get() = MsgComm.GroupInfo(
- groupCode = if (isToGroup) targetGroupCode!! else 0,
- groupCard = senderName
- )
- val isToGroup: Boolean get() = contact is Group
- suspend fun MessageChain.convertToLongMessageIfNeeded(
- step: SendMessageStep,
- ): MessageChain {
- suspend fun sendLongImpl(): MessageChain {
- val resId = uploadLongMessageHighway(this)
- return this + RichMessage.longMessage(
- brief = takeContent(27),
- resId = resId,
- timeSeconds = currentTimeSeconds()
- ) // LongMessageInternal replaces all contents and preserves metadata
- }
- return when (step) {
- SendMessageStep.FIRST -> {
- // 只需要在第一次发送的时候验证长度
- // 后续重试直接跳过
- if (contains(ForceAsLongMessage)) {
- return sendLongImpl()
- }
- if (!contains(IgnoreLengthCheck)) {
- verifyLength(this, contact)
- }
- this
- }
- SendMessageStep.LONG_MESSAGE -> {
- if (contains(DontAsLongMessage)) this // fragmented
- else sendLongImpl()
- }
- SendMessageStep.FRAGMENTED -> this
- }
- }
- /**
- * Final process. Convert transformed message to protocol internals and transfer to server
- */
- suspend fun sendMessagePacket(
- originalMessage: Message,
- transformedMessage: MessageChain,
- finalMessage: MessageChain,
- isMiraiInternal: Boolean,
- step: SendMessageStep,
- ): MessageReceipt<C> {
- bot.components[MessageSvcSyncer].joinSync()
- val group = contact
- var source: Deferred<OnlineMessageSource.Outgoing>? = null
- bot.network.run {
- sendMessageMultiProtocol(
- bot.client, finalMessage,
- fragmented = step == SendMessageStep.FRAGMENTED
- ) { source = it }.forEach { packet ->
- when (val resp = packet.sendAndExpect<Packet>()) {
- is MessageSvcPbSendMsg.Response -> {
- if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
- return when (step) {
- SendMessageStep.FIRST -> {
- sendMessageImpl(
- originalMessage,
- transformedMessage,
- isMiraiInternal,
- SendMessageStep.LONG_MESSAGE,
- )
- }
- SendMessageStep.LONG_MESSAGE -> {
- sendMessageImpl(
- originalMessage,
- transformedMessage,
- isMiraiInternal,
- SendMessageStep.FRAGMENTED,
- )
- }
- else -> {
- throw MessageTooLargeException(
- group,
- originalMessage,
- finalMessage,
- "Message '${finalMessage.content.take(10)}' is too large."
- )
- }
- }
- }
- if (resp is MessageSvcPbSendMsg.Response.ServiceUnavailable) {
- throw IllegalStateException("Send message to $contact failed, server service is unavailable.")
- }
- if (resp is MessageSvcPbSendMsg.Response.Failed) {
- val contact = contact
- when (resp.resultType) {
- 120 -> if (contact is Group) throw BotIsBeingMutedException(contact, originalMessage)
- 121 -> if (AtAll in finalMessage) throw IllegalStateException("Send message to $contact failed, reached maximum AtAll times limit.")
- 299 -> if (contact is Group) throw SendMessageFailedException(
- contact,
- SendMessageFailedException.Reason.GROUP_CHAT_LIMITED,
- originalMessage
- )
- }
- }
- check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
- "Send message failed: $resp"
- }
- }
- is MusicSharePacket.Response -> {
- resp.pkg.checkSuccess("send music share")
- source = CompletableDeferred(constructSourceForSpecialMessage(finalMessage, 3116))
- }
- // is CommonOidbResponse<*> -> {
- // when (resp.toResult("send message").getOrThrow()) {
- // is Oidb0x6d9.FeedsRspBody -> {
- // }
- // }
- // }
- }
- }
- val sourceAwait = source?.await() ?: error("Internal error: source is not initialized")
- try {
- sourceAwait.ensureSequenceIdAvailable()
- } catch (e: Exception) {
- bot.network.logger.warning(
- "Timeout awaiting sequenceId for message(${finalMessage.content.take(10)}). Some features may not work properly",
- e
- )
- }
- return sourceAwait.createMessageReceipt(contact, true)
- }
- }
- private suspend fun sendMessageMultiProtocol(
- client: QQAndroidClient,
- message: MessageChain,
- fragmented: Boolean,
- sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit,
- ): List<OutgoingPacket> {
- message.takeSingleContent<MusicShare>()?.let { musicShare ->
- return listOf(
- MusicSharePacket(
- client, musicShare, contact.id,
- targetKind = if (isToGroup) MessageSourceKind.GROUP else MessageSourceKind.FRIEND // always FRIEND
- )
- )
- }
- message.takeSingleContent<FileMessage>()?.let { file ->
- file.checkIsImpl()
- sourceCallback(contact.async { constructSourceForSpecialMessage(message, 2021) })
- return listOf(FileManagement.Feed(client, contact.id, file.busId, file.id))
- }
- return messageSvcSendMessage(client, contact, message, fragmented, sourceCallback)
- }
- abstract val messageSvcSendMessage: (
- client: QQAndroidClient,
- contact: C,
- message: MessageChain,
- fragmented: Boolean,
- sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit,
- ) -> List<OutgoingPacket>
- abstract suspend fun constructSourceForSpecialMessage(
- finalMessage: MessageChain,
- fromAppId: Int,
- ): OnlineMessageSource.Outgoing
- open suspend fun uploadLongMessageHighway(
- chain: MessageChain,
- ): String = with(contact) {
- return getMiraiImpl().uploadMessageHighway(
- bot, this@SendMessageHandler,
- listOf(
- ForwardMessage.Node(
- senderId = bot.id,
- time = currentTimeSeconds().toInt(),
- messageChain = chain,
- senderName = bot.nick
- )
- ),
- true
- )
- }
- open suspend fun preConversionTransformedMessage(message: Message): Message = message
- open suspend fun conversionMessageChain(chain: MessageChain): MessageChain = chain
- open suspend fun postTransformActions(chain: MessageChain) {
- }
- }
- /**
- * 处理需要 `suspend` 操作的消息转换. 这个转换只会在发送消息时进行, 而不会在处理合并转发 [net.mamoe.mirai.internal.network.protocol.packet.chat.calculateValidationData] 等其他操作时进行.
- * 在发包前还会进行最后的 [net.mamoe.mirai.internal.message.toRichTextElems] 转换, 这个转换会为所有操作使用.
- *
- * - [ForwardMessage] -> [ForwardMessageInternal] (by uploading through highway)
- * - ... any others for future
- */
- internal suspend fun <C : Contact> SendMessageHandler<C>.transformSpecialMessages(message: Message): MessageChain {
- suspend fun processForwardMessage(
- forward: ForwardMessage,
- ): ForwardMessageInternal {
- if (!(message is MessageChain && message.contains(IgnoreLengthCheck))) {
- check(forward.nodeList.size <= 200) {
- throw MessageTooLargeException(
- contact, forward, forward,
- "ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
- )
- }
- val tmp = ArrayList<SingleMessage>(
- forward.nodeList.sumOf { it.messageChain.size }
- )
- forward.nodeList.forEach { tmp.addAll(it.messageChain) }
- // toMessageChain will lose some element
- @Suppress("INVISIBLE_MEMBER")
- createMessageChainImplOptimized(tmp).verifyLength(forward, contact)
- }
- val resId = getMiraiImpl().uploadMessageHighway(
- bot = contact.bot,
- sendMessageHandler = this,
- message = forward.nodeList,
- isLong = false,
- )
- return RichMessage.forwardMessage(
- resId = resId,
- fileName = currentTimeSeconds().toString(),
- forwardMessage = forward,
- )
- }
- // loses MessageMetadata and other message types but fine for now.
- return message.takeSingleContent<ForwardMessage>()?.let { processForwardMessage(it) }?.toMessageChain()
- ?: message.toMessageChain()
- }
- /**
- * Send a message, and covert messages
- *
- * Don't recall this function.
- */
- internal suspend fun <C : Contact> SendMessageHandler<C>.sendMessage(
- originalMessage: Message,
- transformedMessage: Message,
- isMiraiInternal: Boolean,
- step: SendMessageStep,
- ): MessageReceipt<C> = sendMessageImpl(
- originalMessage,
- conversionMessageChain(
- transformSpecialMessages(
- preConversionTransformedMessage(transformedMessage)
- )
- ),
- isMiraiInternal,
- step
- )
- /**
- * Might be recalled with [transformedMessage] `is` [LongMessageInternal] if length estimation failed (sendMessagePacket)
- */
- private suspend fun <C : Contact> SendMessageHandler<C>.sendMessageImpl(
- originalMessage: Message,
- transformedMessage: MessageChain,
- isMiraiInternal: Boolean,
- step: SendMessageStep,
- ): MessageReceipt<C> { // Result cannot be in interface.
- if (!isMiraiInternal && step == SendMessageStep.FIRST) {
- transformedMessage.verifySendingValid()
- }
- val chain = transformedMessage.convertToLongMessageIfNeeded(step)
- chain.findIsInstance<QuoteReply>()?.source?.ensureSequenceIdAvailable()
- postTransformActions(chain)
- return sendMessagePacket(originalMessage, transformedMessage, chain, isMiraiInternal, step)
- }
- internal sealed class UserSendMessageHandler<C : AbstractUser>(
- override val contact: C,
- ) : SendMessageHandler<C>() {
- override val senderName: String get() = bot.nick
- override suspend fun constructSourceForSpecialMessage(
- finalMessage: MessageChain,
- fromAppId: Int,
- ): OnlineMessageSource.Outgoing {
- throw UnsupportedOperationException("Sending MusicShare or FileMessage to User is not yet supported")
- }
- }
- internal class FriendSendMessageHandler(
- contact: FriendImpl,
- ) : UserSendMessageHandler<FriendImpl>(contact) {
- override val messageSvcSendMessage: (client: QQAndroidClient, contact: FriendImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
- MessageSvcPbSendMsg::createToFriend
- override suspend fun constructSourceForSpecialMessage(
- finalMessage: MessageChain,
- fromAppId: Int
- ): OnlineMessageSource.Outgoing {
- val receipt: SendPrivateMessageReceipt = nextEventOrNull(3000) {
- it.bot === bot && it.fromAppId == fromAppId
- } ?: SendPrivateMessageReceipt.EMPTY
- return OnlineMessageSourceToFriendImpl(
- internalIds = intArrayOf(receipt.messageRandom),
- sequenceIds = intArrayOf(receipt.sequenceId),
- sender = bot,
- target = contact,
- time = bot.clock.server.currentTimeSeconds().toInt(),
- originalMessage = finalMessage
- )
- }
- }
- internal class StrangerSendMessageHandler(
- contact: StrangerImpl,
- ) : UserSendMessageHandler<StrangerImpl>(contact) {
- override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
- MessageSvcPbSendMsg::createToStranger
- }
- internal class GroupTempSendMessageHandler(
- contact: NormalMemberImpl,
- ) : UserSendMessageHandler<NormalMemberImpl>(contact) {
- override val messageSvcSendMessage: (client: QQAndroidClient, contact: NormalMemberImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
- MessageSvcPbSendMsg::createToTemp
- }
- internal open class GroupSendMessageHandler(
- override val contact: GroupImpl,
- ) : SendMessageHandler<GroupImpl>() {
- override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
- MessageSvcPbSendMsg::createToGroup
- override val senderName: String
- get() = contact.botAsMember.nameCardOrNick
- override suspend fun conversionMessageChain(chain: MessageChain): MessageChain = chain.map { element ->
- when (element) {
- is OfflineGroupImage -> {
- contact.fixImageFileId(element)
- element
- }
- is FriendImage -> {
- contact.updateFriendImageForGroupMessage(element)
- }
- else -> element
- }
- }.toMessageChain()
- override suspend fun constructSourceForSpecialMessage(
- finalMessage: MessageChain,
- fromAppId: Int,
- ): OnlineMessageSource.Outgoing {
- val receipt: SendGroupMessageReceipt = nextEventOrNull(3000) {
- it.bot === bot && it.fromAppId == fromAppId
- } ?: SendGroupMessageReceipt.EMPTY
- return OnlineMessageSourceToGroupImpl(
- contact,
- internalIds = intArrayOf(receipt.messageRandom),
- providedSequenceIds = intArrayOf(receipt.sequenceId),
- sender = bot,
- target = contact,
- time = bot.clock.server.currentTimeSeconds().toInt(),
- originalMessage = finalMessage
- )
- }
- companion object {
- private suspend fun GroupImpl.fixImageFileId(image: OfflineGroupImage) {
- bot.components[ImagePatcher].patchOfflineGroupImage(this, image)
- }
- private suspend fun GroupImpl.updateFriendImageForGroupMessage(image: FriendImage): OfflineGroupImage {
- return bot.components[ImagePatcher].patchFriendImageToGroupImage(this, image)
- }
- }
- }
|