diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index a78147e5b..d4a47235d 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -649,25 +649,6 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) } } - internal open suspend fun uploadMessageHighway( - bot: Bot, - sendMessageHandler: SendMessageHandler<*>, - message: Collection<ForwardMessage.INode>, - isLong: Boolean, - ): String { - bot.asQQAndroidBot() - message.forEach { - it.messageChain.ensureSequenceIdAvailable() - } - val uploader = MultiMsgUploader( - client = bot.client, - isLong = isLong, - handler = sendMessageHandler, - ).also { it.emitMain(message) } - - return uploader.uploadAndReturnResId() - } - override suspend fun solveNewFriendRequestEvent( bot: Bot, eventId: Long, diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt index 2440a0a9e..7d67d1968 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. @@ -9,9 +9,11 @@ package net.mamoe.mirai.internal.contact -import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.utils.childScopeContext +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext internal abstract class AbstractContact( @@ -19,4 +21,21 @@ internal abstract class AbstractContact( parentCoroutineContext: CoroutineContext, ) : Contact { final override val coroutineContext: CoroutineContext = parentCoroutineContext.childScopeContext() +} + +internal val Contact.userIdOrNull: Long? get() = if (this is User) this.id else null +internal val Contact.groupIdOrNull: Long? get() = if (this is Group) this.id else null +internal val Contact.groupUinOrNull: Long? get() = if (this is Group) this.uin else null +internal val ContactOrBot.uin: Long + get() = when (this) { + is Group -> uin + is User -> uin + is OtherClient -> bot.uin + is Bot -> id + else -> this.id + } + +internal fun Contact.impl(): AbstractContact { + contract { returns() implies (this@impl is AbstractContact) } + return this as AbstractContact } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt index 1927d6809..f650a6ae4 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt @@ -21,6 +21,10 @@ import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag import net.mamoe.mirai.internal.message.image.* +import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade +import net.mamoe.mirai.internal.message.protocol.outgoing.HighwayUploader +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy +import net.mamoe.mirai.internal.network.component.buildComponentStorage import net.mamoe.mirai.internal.network.components.BdhSession import net.mamoe.mirai.internal.network.highway.ChannelKind import net.mamoe.mirai.internal.network.highway.Highway @@ -98,15 +102,14 @@ internal sealed class AbstractUser( return when (resp) { is LongConn.OffPicUp.Response.FileExists -> { - val imageType = getImageType(resp.imageInfo.fileType) - .takeIf { it != ExternalResource.DEFAULT_FORMAT_NAME } - ?: resource.formatName + val imageType = + getImageType(resp.imageInfo.fileType).takeIf { it != ExternalResource.DEFAULT_FORMAT_NAME } + ?: resource.formatName resp.imageInfo.run { OfflineFriendImage( imageId = generateImageIdFromResourceId( - resourceId = resp.resourceId, - format = imageType + resourceId = resp.resourceId, format = imageType ) ?: kotlin.run { if (resp.imageInfo.fileMd5.size == 16) { generateImageId(resp.imageInfo.fileMd5, imageType) @@ -164,8 +167,7 @@ internal sealed class AbstractUser( } is ImgStore.GroupPicUp.Response.RequireUpload -> { // val servers = response.uploadIpList.zip(response.uploadPortList) - Highway.uploadResourceBdh( - bot = bot, + Highway.uploadResourceBdh(bot = bot, resource = resource, kind = PRIVATE_IMAGE, commandId = 2, @@ -176,8 +178,7 @@ internal sealed class AbstractUser( ssoAddresses = response.uploadIpList.zip(response.uploadPortList) .toMutableSet(), ) - } - ) + }) } } }.recoverCatchingSuppressed { @@ -198,9 +199,9 @@ internal sealed class AbstractUser( resourceKind = PRIVATE_IMAGE, channelKind = ChannelKind.HTTP ) { ip, port -> - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - Mirai.Http.postImage( - serverIp = ip, serverPort = port, + @Suppress("DEPRECATION", "DEPRECATION_ERROR") Mirai.Http.postImage( + serverIp = ip, + serverPort = port, htcmd = "0x6ff0070", uin = bot.id, groupcode = null, @@ -210,8 +211,7 @@ internal sealed class AbstractUser( } }.recoverCatchingSuppressed { // try upload by http on fallback server - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - Mirai.Http.postImage( + @Suppress("DEPRECATION", "DEPRECATION_ERROR") Mirai.Http.postImage( serverIp = "htdata2.qq.com", htcmd = "0x6ff0070", uin = bot.id, @@ -242,9 +242,10 @@ internal sealed class AbstractUser( } } -@Suppress("DuplicatedCode") -internal suspend fun <C : User> SendMessageHandler<out C>.sendMessageImpl( + +internal suspend fun <C : AbstractContact> C.sendMessageImpl( message: Message, + messageProtocolStrategy: MessageProtocolStrategy<C>, preSendEventConstructor: (C, Message) -> MessagePreSendEvent, postSendEventConstructor: (C, MessageChain, Throwable?, MessageReceipt<C>?) -> MessagePostSendEvent<C>, ): MessageReceipt<C> { @@ -252,20 +253,24 @@ internal suspend fun <C : User> SendMessageHandler<out C>.sendMessageImpl( message.anyIsInstance<MiraiInternalMessageFlag>() } else false - require(isMiraiInternal || !message.isContentEmpty()) { "message is empty" } + require(!message.isContentEmpty()) { "message is empty" } - val chain = contact.broadcastMessagePreSendEvent(message, isMiraiInternal, preSendEventConstructor) + val chain = broadcastMessagePreSendEvent(message, isMiraiInternal, preSendEventConstructor) - val result = this - .runCatching { sendMessage(message, chain, isMiraiInternal, SendMessageStep.FIRST) } + val result = kotlin.runCatching { + MessageProtocolFacade.preprocessAndSendOutgoing(this, message, buildComponentStorage { + set(MessageProtocolStrategy, messageProtocolStrategy) + set(HighwayUploader, HighwayUploader.Default) + }) + } if (result.isSuccess) { // logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source - contact.logMessageSent(chain) + bot.logger.verbose("$this <- $chain".replaceMagicCodes()) } if (!isMiraiInternal) { - postSendEventConstructor(contact, chain, result.exceptionOrNull(), result.getOrNull()).broadcast() + postSendEventConstructor(this, chain, result.exceptionOrNull(), result.getOrNull()).broadcast() } return result.getOrThrow() diff --git a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt index 436befa62..dbe4f9cd1 100644 --- a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt @@ -24,6 +24,8 @@ import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.info.FriendInfoImpl import net.mamoe.mirai.internal.contact.roaming.RoamingMessagesImplFriend import net.mamoe.mirai.internal.message.data.OfflineAudioImpl +import net.mamoe.mirai.internal.message.protocol.outgoing.FriendMessageProtocolStrategy +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy import net.mamoe.mirai.internal.network.highway.* import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody @@ -64,6 +66,9 @@ internal class FriendImpl( ) : Friend, AbstractUser(bot, parentCoroutineContext, info) { override var nick: String by info::nick override var remark: String by info::remark + + private val messageProtocolStrategy: MessageProtocolStrategy<FriendImpl> = FriendMessageProtocolStrategy(this) + override suspend fun delete() { check(bot.friends[id] != null) { "Friend $id had already been deleted" @@ -73,12 +78,13 @@ internal class FriendImpl( } } - - private val handler: FriendSendMessageHandler by lazy { FriendSendMessageHandler(this) } - - @Suppress("DuplicatedCode") override suspend fun sendMessage(message: Message): MessageReceipt<Friend> { - return handler.sendMessageImpl(message, ::FriendMessagePreSendEvent, ::FriendMessagePostSendEvent) + return sendMessageImpl( + message, + messageProtocolStrategy, + ::FriendMessagePreSendEvent, + ::FriendMessagePostSendEvent.cast() + ) } override fun toString(): String = "Friend($id)" diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index db90240fb..8ab5ad9b5 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -13,6 +13,7 @@ package net.mamoe.mirai.internal.contact import kotlinx.atomicfu.atomic +import net.mamoe.mirai.Bot import net.mamoe.mirai.LowLevelApi import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.* @@ -28,11 +29,12 @@ import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.data.OfflineAudioImpl -import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag import net.mamoe.mirai.internal.message.image.OfflineGroupImage import net.mamoe.mirai.internal.message.image.calculateImageInfo import net.mamoe.mirai.internal.message.image.getIdByImageType import net.mamoe.mirai.internal.message.image.getImageTypeById +import net.mamoe.mirai.internal.message.protocol.outgoing.GroupMessageProtocolStrategy +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy import net.mamoe.mirai.internal.network.components.BdhSession import net.mamoe.mirai.internal.network.handler.logger import net.mamoe.mirai.internal.network.highway.ChannelKind @@ -60,7 +62,6 @@ import net.mamoe.mirai.utils.* import java.util.concurrent.ConcurrentLinkedQueue import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext -import kotlin.time.ExperimentalTime internal fun GroupImpl.Companion.checkIsInstance(instance: Group) { contract { returns() implies (instance is GroupImpl) } @@ -114,6 +115,9 @@ private val logger by lazy { MiraiLogger.Factory.create(GroupImpl::class.java, "Group") } +internal fun Bot.nickIn(context: Contact): String = + if (context is Group) context.botAsMember.nameCardOrNick else bot.nick + @Suppress("PropertyName") internal class GroupImpl constructor( bot: QQAndroidBot, @@ -149,6 +153,8 @@ internal class GroupImpl constructor( val groupPkgMsgParsingCache = GroupPkgMsgParsingCache() + private val messageProtocolStrategy: MessageProtocolStrategy<GroupImpl> = GroupMessageProtocolStrategy(this) + override suspend fun quit(): Boolean { check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" } @@ -178,29 +184,15 @@ internal class GroupImpl constructor( } override suspend fun sendMessage(message: Message): MessageReceipt<Group> { - val isMiraiInternal = if (message is MessageChain) { - message.anyIsInstance<MiraiInternalMessageFlag>() - } else false - - require(isMiraiInternal || !message.isContentEmpty()) { "message is empty" } check(!isBotMuted) { throw BotIsBeingMutedException(this, message) } - - val chain = broadcastMessagePreSendEvent(message, isMiraiInternal, ::GroupMessagePreSendEvent) - - val result = GroupSendMessageHandler(this) - .runCatching { sendMessage(message, chain, isMiraiInternal, SendMessageStep.FIRST) } - - if (result.isSuccess) { - // logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source - logMessageSent(chain) - } - if (!isMiraiInternal) { - GroupMessagePostSendEvent(this, chain, result.exceptionOrNull(), result.getOrNull()).broadcast() - } - return result.getOrThrow() + return sendMessageImpl( + message, + messageProtocolStrategy, + ::GroupMessagePreSendEvent, + ::GroupMessagePostSendEvent.cast() + ) } - @OptIn(ExperimentalTime::class) override suspend fun uploadImage(resource: ExternalResource): Image = resource.withAutoClose { if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) { throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup") diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt index b9308094e..237e3b992 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt @@ -45,5 +45,21 @@ internal suspend fun <C : Contact> C.broadcastMessagePreSendEvent( internal enum class SendMessageStep { - FIRST, LONG_MESSAGE, FRAGMENTED + FIRST { + override fun nextStepOrNull(): SendMessageStep { + return LONG_MESSAGE + } + }, + LONG_MESSAGE { + override fun nextStepOrNull(): SendMessageStep { + return FRAGMENTED + } + }, + FRAGMENTED { + override fun nextStepOrNull(): SendMessageStep? { + return null + } + }; + + abstract fun nextStepOrNull(): SendMessageStep? } diff --git a/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt b/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt index b30d5f574..a6155aa18 100644 --- a/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt @@ -19,6 +19,8 @@ import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.internal.message.protocol.outgoing.GroupTempMessageProtocolStrategy +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToTempImpl import net.mamoe.mirai.internal.message.source.createMessageReceipt import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement @@ -41,18 +43,19 @@ internal class NormalMemberImpl constructor( override val joinTimestamp: Int get() = info.joinTimestamp override val lastSpeakTimestamp: Int get() = info.lastSpeakTimestamp - override fun toString(): String = "NormalMember($id)" + private val messageProtocolStrategy: MessageProtocolStrategy<NormalMemberImpl> = GroupTempMessageProtocolStrategy - private val handler: GroupTempSendMessageHandler by lazy { GroupTempSendMessageHandler(this) } + override fun toString(): String = "NormalMember($id)" @Suppress("DuplicatedCode") override suspend fun sendMessage(message: Message): MessageReceipt<NormalMember> { return asFriendOrNull()?.sendMessage(message)?.convert() ?: asStrangerOrNull()?.sendMessage(message)?.convert() - ?: handler.sendMessageImpl<NormalMember>( + ?: sendMessageImpl( message = message, preSendEventConstructor = ::GroupTempMessagePreSendEvent, postSendEventConstructor = ::GroupTempMessagePostSendEvent.cast(), + messageProtocolStrategy = messageProtocolStrategy ) } diff --git a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt index 4a6f58c9f..e69de29bb 100644 --- a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt @@ -1,496 +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 - */ - -package net.mamoe.mirai.internal.contact - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.withTimeoutOrNull -import net.mamoe.mirai.Bot -import net.mamoe.mirai.contact.* -import net.mamoe.mirai.event.EventPriority -import net.mamoe.mirai.event.GlobalEventChannel -import net.mamoe.mirai.event.nextEvent -import net.mamoe.mirai.internal.asQQAndroidBot -import net.mamoe.mirai.internal.getMiraiImpl -import net.mamoe.mirai.internal.message.* -import net.mamoe.mirai.internal.message.data.ForwardMessageInternal -import net.mamoe.mirai.internal.message.data.checkIsImpl -import net.mamoe.mirai.internal.message.data.forwardMessage -import net.mamoe.mirai.internal.message.data.longMessage -import net.mamoe.mirai.internal.message.flags.DontAsLongMessage -import net.mamoe.mirai.internal.message.flags.ForceAsLongMessage -import net.mamoe.mirai.internal.message.flags.IgnoreLengthCheck -import net.mamoe.mirai.internal.message.image.FriendImage -import net.mamoe.mirai.internal.message.image.OfflineGroupImage -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl -import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToGroupImpl -import net.mamoe.mirai.internal.message.source.createMessageReceipt -import net.mamoe.mirai.internal.message.source.ensureSequenceIdAvailable -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 - -internal fun ContactOrBot.inferMessageSourceKind(): MessageSourceKind { - return when (this) { - is Group -> MessageSourceKind.GROUP - is Friend -> MessageSourceKind.FRIEND - is Member -> MessageSourceKind.TEMP - is Stranger -> MessageSourceKind.STRANGER - is Bot -> MessageSourceKind.FRIEND - else -> error("Unsupported contact: $this") - } -} - -/** - * 处理 mirai 消息系统 `Message` 到协议数据结构的转换. - * - * 外部调用 [sendMessageImpl] - */ -internal abstract class SendMessageHandler<C : Contact> { - abstract val contact: C - abstract val senderName: String - - val messageSourceKind: MessageSourceKind - get() { - return contact.inferMessageSourceKind() - } - - 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 = bot.network.sendAndExpect<Packet>(packet, 5000, 2)) { - 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 SendMessageFailedException( - contact, - SendMessageFailedException.Reason.AT_ALL_LIMITED, - originalMessage - ) - 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.protocol.MessageProtocolFacade.encode] 转换, 这个转换会为所有操作使用. - * - * - [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}" - ) - } - sequence { - forward.nodeList.forEach { yieldAll(it.messageChain) } - }.asIterable().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 = withTimeoutOrNull(3000) { - GlobalEventChannel.parentScope(this).nextEvent(EventPriority.MONITOR) { - 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 = withTimeoutOrNull(3000) { - GlobalEventChannel.parentScope(this).nextEvent(EventPriority.MONITOR) { - 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) - } - } -} diff --git a/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt b/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt index acc58327e..dc4be0328 100644 --- a/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt @@ -25,6 +25,8 @@ import net.mamoe.mirai.data.StrangerInfo import net.mamoe.mirai.event.events.StrangerMessagePostSendEvent import net.mamoe.mirai.event.events.StrangerMessagePreSendEvent import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy +import net.mamoe.mirai.internal.message.protocol.outgoing.StrangerMessageProtocolStrategy import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToStrangerImpl import net.mamoe.mirai.internal.message.source.createMessageReceipt import net.mamoe.mirai.internal.network.protocol.packet.list.StrangerList @@ -59,13 +61,14 @@ internal class StrangerImpl( } } - private val handler: StrangerSendMessageHandler by lazy { StrangerSendMessageHandler(this) } + private val messageProtocolStrategy: MessageProtocolStrategy<StrangerImpl> = StrangerMessageProtocolStrategy @Suppress("DuplicatedCode") override suspend fun sendMessage(message: Message): MessageReceipt<Stranger> { return asFriendOrNull()?.sendMessage(message)?.convert() - ?: handler.sendMessageImpl<Stranger>( + ?: sendMessageImpl( message = message, + messageProtocolStrategy = messageProtocolStrategy, preSendEventConstructor = ::StrangerMessagePreSendEvent, postSendEventConstructor = ::StrangerMessagePostSendEvent.cast() ) diff --git a/mirai-core/src/commonMain/kotlin/contact/util.kt b/mirai-core/src/commonMain/kotlin/contact/util.kt index d2cf5b7dc..3af4d7850 100644 --- a/mirai-core/src/commonMain/kotlin/contact/util.kt +++ b/mirai-core/src/commonMain/kotlin/contact/util.kt @@ -13,68 +13,15 @@ package net.mamoe.mirai.internal.contact import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* -import net.mamoe.mirai.internal.message.data.FileMessageImpl -import net.mamoe.mirai.internal.message.data.LongMessageInternal -import net.mamoe.mirai.internal.utils.estimateLength import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.cast +import net.mamoe.mirai.utils.verbose internal inline val Group.uin: Long get() = this.cast<GroupImpl>().uin internal inline val Group.groupCode: Long get() = this.id internal inline val User.uin: Long get() = this.id internal inline val Bot.uin: Long get() = this.id -internal fun Contact.logMessageSent(message: Message) { - if (message !is LongMessageInternal) { - bot.logger.verbose("$this <- $message".replaceMagicCodes()) - } -} - -internal fun Iterable<SingleMessage>.countImages(): Int = this.count { it is Image } - -private val logger by lazy { MiraiLogger.Factory.create(SendMessageHandler::class) } - -private val ALLOW_SENDING_FILE_MESSAGE = systemProp("mirai.message.allow.sending.file.message", false) - -internal fun Message.verifySendingValid() { -// fun fail(msg: String): Nothing = throw IllegalArgumentException(msg) - when (this) { - is MessageChain -> { - this.forEach { it.verifySendingValid() } - } - is FileMessage -> { - if (!ALLOW_SENDING_FILE_MESSAGE) { // #1715 - if (this !is FileMessageImpl) error("Customized FileMessage cannot be send") - if (!this.allowSend) error( - "Sending FileMessage is not allowed, as it may cause unexpected results. " + - "Add JVM argument `-Dmirai.message.allow.sending.file.message=true` to disable this check. " + - "Do this only for compatibility!" - ) - } - } - } -} - -internal fun Iterable<SingleMessage>.verifyLength( - originalMessage: Message, target: Contact, -): Int { - val chain = this - val length = estimateLength(target, 15001) - if (length > 15000 || countImages() > 50) { - throw MessageTooLargeException( - target, originalMessage, this.toMessageChain(), - "message(${ - chain.joinToString("", limit = 10).let { rsp -> - if (rsp.length > 100) { - rsp.take(100) + "..." - } else rsp - } - }) is too large. Allow up to 50 images or 5000 chars" - ) - } - return length -} - @Suppress("RemoveRedundantQualifierName") // compiler bug internal fun net.mamoe.mirai.event.events.MessageEvent.logMessageReceived() { fun renderMessage(message: MessageChain): String { @@ -168,7 +115,3 @@ internal fun String.replaceMagicCodes(): String = this internal fun Message.takeContent(length: Int): String = this.toMessageChain().joinToString("", limit = length) { it.content } - -internal inline fun <reified T : MessageContent> Message.takeSingleContent(): T? { - return this as? T ?: this.castOrNull<MessageChain>()?.findIsInstance() -} diff --git a/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt index d0bcd8321..adde2269b 100644 --- a/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt +++ b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt @@ -19,7 +19,7 @@ internal data class ContextualBugReportException( internal fun contextualBugReportException( context: String, - forDebug: String, + forDebug: String?, e: Throwable? = null, additional: String = "", ): ContextualBugReportException { diff --git a/mirai-core/src/commonMain/kotlin/message/data/MessageChainBuilderExt.kt b/mirai-core/src/commonMain/kotlin/message/data/MessageChainBuilderExt.kt new file mode 100644 index 000000000..07ba6f86c --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/data/MessageChainBuilderExt.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.data + +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageChainBuilder +import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.message.data.toMessageChain +import net.mamoe.mirai.message.data.visitor.MessageVisitor +import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit +import net.mamoe.mirai.message.data.visitor.accept +import net.mamoe.mirai.utils.replaceAllKotlin + + +internal fun MessageChainBuilder.acceptChildren(visitor: MessageVisitorUnit) { + forEach { it.accept(visitor) } +} + +internal fun MessageChainBuilder.transformChildren(visitor: MessageVisitor<MessageChainBuilder, SingleMessage>) { + replaceAllKotlin { it.accept(visitor, this) } +} + +internal inline fun MessageChain.transform(trans: (SingleMessage) -> SingleMessage?): MessageChain { + return this.mapNotNull(trans).toMessageChain() +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/data/MessageSourceExt.kt b/mirai-core/src/commonMain/kotlin/message/data/MessageSourceExt.kt new file mode 100644 index 000000000..640afbe49 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/data/MessageSourceExt.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.data + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.message.data.MessageSourceKind + +internal fun ContactOrBot.inferMessageSourceKind(): MessageSourceKind { + return when (this) { + is Group -> MessageSourceKind.GROUP + is Member -> MessageSourceKind.TEMP + is Friend -> MessageSourceKind.FRIEND + is Stranger -> MessageSourceKind.STRANGER + is Bot -> MessageSourceKind.FRIEND + else -> error("Unsupported contact: $this") + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/data/MultiMsgUploader.kt b/mirai-core/src/commonMain/kotlin/message/data/MultiMsgUploader.kt index c0ebd538a..74db79f19 100644 --- a/mirai-core/src/commonMain/kotlin/message/data/MultiMsgUploader.kt +++ b/mirai-core/src/commonMain/kotlin/message/data/MultiMsgUploader.kt @@ -10,11 +10,18 @@ package net.mamoe.mirai.internal.message.data import io.ktor.utils.io.core.* -import net.mamoe.mirai.internal.contact.SendMessageHandler -import net.mamoe.mirai.internal.contact.takeSingleContent +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.internal.contact.groupCode +import net.mamoe.mirai.internal.contact.impl +import net.mamoe.mirai.internal.contact.uin +import net.mamoe.mirai.internal.contact.userIdOrNull import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade +import net.mamoe.mirai.internal.message.protocol.outgoing.HighwayUploader +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy import net.mamoe.mirai.internal.message.source.MessageSourceInternal import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.component.buildComponentStorage import net.mamoe.mirai.internal.network.highway.Highway import net.mamoe.mirai.internal.network.highway.ResourceKind import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody @@ -35,15 +42,21 @@ import kotlin.random.Random internal open class MultiMsgUploader( val client: QQAndroidClient, val isLong: Boolean, - val handler: SendMessageHandler<*>, val tmpRand: Random = Random.Default, + val facade: MessageProtocolFacade, + val contact: Contact, + val strategy: MessageProtocolStrategy<*>, + val senderName: String ) { protected open fun newUploader(): MultiMsgUploader = MultiMsgUploader( isLong = isLong, - handler = handler, client = client, tmpRand = tmpRand, + facade = facade, + contact = contact, + senderName = senderName, + strategy = strategy ) val mainMsg = mutableListOf<MsgComm.Msg>() @@ -123,11 +136,14 @@ internal open class MultiMsgUploader( msgs.forEach { msg -> var msgChain = msg.messageChain - msgChain.takeSingleContent<ForwardMessage>()?.let { nestedForward -> + msgChain[ForwardMessage]?.let { nestedForward -> msgChain = convertNestedForwardMessage(nestedForward, msgChain) } - msgChain = handler.conversionMessageChain(msgChain) + msgChain = facade.preprocess(contact.impl(), msgChain, buildComponentStorage { + set(MessageProtocolStrategy, strategy) + set(HighwayUploader, HighwayUploader.Default) + }) var seq: Int = -1 var uid: Int = -1 @@ -149,7 +165,7 @@ internal open class MultiMsgUploader( msgHead = MsgComm.MsgHead( fromUin = msg.senderId, toUin = if (isLong) { - handler.targetUserUin ?: 0 + contact.userIdOrNull ?: 0 } else 0, msgSeq = seq, msgTime = msg.time, @@ -159,13 +175,17 @@ internal open class MultiMsgUploader( msgId = 1, ), msgType = 82, // troop, - groupInfo = handler.run { msg.groupInfo }, + groupInfo = if (contact is Group) MsgComm.GroupInfo( + groupCode = contact.groupCode, + groupCard = senderName // Cinnamon + ) else null, isSrcMsg = false, ), msgBody = ImMsgBody.MsgBody( richText = ImMsgBody.RichText( elems = MessageProtocolFacade.encode( - msgChain, messageTarget = handler.contact, + msgChain, + messageTarget = contact, withGeneralFlags = false, isForward = true ) @@ -201,11 +221,11 @@ internal open class MultiMsgUploader( buType = if (isLong) 1 else 2, client = client, messageData = data, - dstUin = handler.targetUin - ), 5000, 2 + dstUin = contact.uin + ) ) - val resId: String + lateinit var resId: String when (response) { is MultiMsg.ApplyUp.Response.MessageTooLarge -> error( @@ -221,7 +241,7 @@ internal open class MultiMsgUploader( msgUpReq = listOf( LongMsg.MsgUpReq( msgType = 3, // group - dstUin = handler.targetUin, + dstUin = contact.uin, msgId = 0, msgUkey = response.proto.msgUkey, needCache = 0, @@ -246,6 +266,6 @@ internal open class MultiMsgUploader( } } - return resId + return resId // this must be initialized, 'lateinit' due to IDE complaint } } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt index c4c20e16f..0bc072a3c 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocol.kt @@ -11,6 +11,10 @@ package net.mamoe.mirai.internal.message.protocol import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoder +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePostprocessor +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePreprocessor +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageSender +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageTransformer import net.mamoe.mirai.message.data.SingleMessage import kotlin.reflect.KClass @@ -29,6 +33,7 @@ internal abstract class MessageProtocol( const val PRIORITY_CONTENT: UInt = 1000u const val PRIORITY_IGNORE: UInt = 500u const val PRIORITY_UNSUPPORTED: UInt = 100u + const val PRIORITY_GENERAL_SENDER: UInt = 100u } object PriorityComparator : Comparator<MessageProtocol> { @@ -55,6 +60,12 @@ internal abstract class ProcessorCollector { abstract fun <T : SingleMessage> add(encoder: MessageEncoder<T>, elementType: KClass<T>) abstract fun add(decoder: MessageDecoder) + + + abstract fun add(preprocessor: OutgoingMessagePreprocessor) + abstract fun add(transformer: OutgoingMessageTransformer) + abstract fun add(sender: OutgoingMessageSender) + abstract fun add(postprocessor: OutgoingMessagePostprocessor) } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt index 35fb5c496..fdf3fab06 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/MessageProtocolFacade.kt @@ -11,26 +11,42 @@ package net.mamoe.mirai.internal.message.protocol import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.ContactOrBot +import net.mamoe.mirai.internal.contact.AbstractContact +import net.mamoe.mirai.internal.contact.SendMessageStep +import net.mamoe.mirai.internal.contact.impl import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep import net.mamoe.mirai.internal.message.EmptyRefineContext import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight import net.mamoe.mirai.internal.message.RefineContext +import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.protocol.decode.* import net.mamoe.mirai.internal.message.protocol.encode.* +import net.mamoe.mirai.internal.message.protocol.outgoing.* +import net.mamoe.mirai.internal.network.component.ComponentStorage import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.utils.runCoroutineInPlace +import net.mamoe.mirai.internal.utils.structureToString +import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor import net.mamoe.mirai.message.data.visitor.accept +import net.mamoe.mirai.utils.MutableTypeSafeMap import net.mamoe.mirai.utils.buildTypeSafeMap +import net.mamoe.mirai.utils.castUp import java.util.* import kotlin.reflect.KClass internal interface MessageProtocolFacade { val encoderPipeline: MessageEncoderPipeline val decoderPipeline: MessageDecoderPipeline + val preprocessorPipeline: OutgoingMessagePipeline + val outgoingPipeline: OutgoingMessagePipeline + val loaded: List<MessageProtocol> + /** + * Encode high-level [MessageChain] to give list of low-level and protocol-specific [ImMsgBody.Elem]s. + */ fun encode( chain: MessageChain, messageTarget: ContactOrBot?, // for At.display, QuoteReply, Image, and more. @@ -38,6 +54,11 @@ internal interface MessageProtocolFacade { isForward: Boolean = false, // is inside forward, for At.display ): List<ImMsgBody.Elem> + /** + * Decode list of low-level and protocol-specific [ImMsgBody.Elem]s to give a high-level [MessageChain]. + * + * [SingleMessage]s are appended to the [builder]. + */ fun decode( elements: List<ImMsgBody.Elem>, groupIdOrZero: Long, @@ -46,6 +67,41 @@ internal interface MessageProtocolFacade { builder: MessageChainBuilder, ) + + /** + * Pre-process a message + * @see OutgoingMessagePreprocessor + */ + suspend fun <C : AbstractContact> preprocess( + target: C, + message: Message, + components: ComponentStorage, + ): MessageChain + + /** + * Send a message + * @see OutgoingMessageProcessor + */ + suspend fun <C : AbstractContact> sendOutgoing( + target: C, + message: Message, + components: ComponentStorage, + ): MessageReceipt<C> + + /** + * Preprocess and send a message + * @see OutgoingMessagePreprocessor + * @see OutgoingMessageProcessor + */ + suspend fun <C : AbstractContact> preprocessAndSendOutgoing( + target: C, + message: Message, + components: ComponentStorage, + ): MessageReceipt<C> + + /** + * Decode list of low-level and protocol-specific [ImMsgBody.Elem]s to give a high-level [MessageChain]. + */ fun decode( elements: List<ImMsgBody.Elem>, groupIdOrZero: Long, @@ -53,6 +109,11 @@ internal interface MessageProtocolFacade { bot: Bot, ): MessageChain = buildMessageChain { decode(elements, groupIdOrZero, messageSourceKind, bot, this) } + fun copy(): MessageProtocolFacade + + /** + * The default global instance. + */ companion object INSTANCE : MessageProtocolFacade by MessageProtocolFacadeImpl() } @@ -74,10 +135,12 @@ internal suspend fun MessageProtocolFacade.decodeAndRefineDeep( internal class MessageProtocolFacadeImpl( - protocols: Iterable<MessageProtocol> = ServiceLoader.load(MessageProtocol::class.java) + private val protocols: Iterable<MessageProtocol> = ServiceLoader.load(MessageProtocol::class.java) ) : MessageProtocolFacade { override val encoderPipeline: MessageEncoderPipeline = MessageEncoderPipelineImpl() override val decoderPipeline: MessageDecoderPipeline = MessageDecoderPipelineImpl() + override val preprocessorPipeline: OutgoingMessagePipeline = OutgoingMessagePipelineImpl() + override val outgoingPipeline: OutgoingMessagePipeline = OutgoingMessagePipelineImpl() override val loaded: List<MessageProtocol> = kotlin.run { val instances: PriorityQueue<MessageProtocol> = protocols @@ -97,6 +160,21 @@ internal class MessageProtocolFacadeImpl( this@MessageProtocolFacadeImpl.decoderPipeline.registerProcessor(MessageDecoderProcessor(decoder)) } + override fun add(preprocessor: OutgoingMessagePreprocessor) { + preprocessorPipeline.registerProcessor(OutgoingMessageProcessorAdapter(preprocessor)) + } + + override fun add(transformer: OutgoingMessageTransformer) { + outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(transformer)) + } + + override fun add(sender: OutgoingMessageSender) { + outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(sender)) + } + + override fun add(postprocessor: OutgoingMessagePostprocessor) { + outgoingPipeline.registerProcessor(OutgoingMessageProcessorAdapter(postprocessor)) + } }) } instances.toList() @@ -122,7 +200,7 @@ internal class MessageProtocolFacadeImpl( chain.accept(object : RecursiveMessageVisitor<Unit>() { override fun visitSingleMessage(message: SingleMessage, data: Unit) { runCoroutineInPlace { - builder.addAll(pipeline.process(message, attributes)) + builder.addAll(pipeline.process(message, attributes).collected) } } }) @@ -146,7 +224,77 @@ internal class MessageProtocolFacadeImpl( } runCoroutineInPlace { - elements.forEach { builder.addAll(pipeline.process(it, attributes)) } + elements.forEach { builder.addAll(pipeline.process(it, attributes).collected) } } } + + override suspend fun <C : AbstractContact> preprocess( + target: C, + message: Message, + components: ComponentStorage + ): MessageChain { + val attributes = createAttributesForOutgoingMessage(target, message, components) + + return preprocessorPipeline.process(message.toMessageChain(), attributes).context.currentMessageChain + } + + override suspend fun <C : AbstractContact> sendOutgoing( + target: C, message: Message, + components: ComponentStorage + ): MessageReceipt<C> { + val attributes = createAttributesForOutgoingMessage(target, message, components) + + val (_, result) = outgoingPipeline.process(message.toMessageChain(), attributes) + + return getSingleReceipt(result, message) + } + + override suspend fun <C : AbstractContact> preprocessAndSendOutgoing( + target: C, + message: Message, + components: ComponentStorage + ): MessageReceipt<C> { + val attributes = createAttributesForOutgoingMessage(target, message, components) + + val (context, _) = preprocessorPipeline.process(message.toMessageChain(), attributes) + val (_, result) = outgoingPipeline.process(message.toMessageChain(), context, attributes) + + return getSingleReceipt(result, message) + } + + override fun copy(): MessageProtocolFacade { + return MessageProtocolFacadeImpl(protocols) + } + + private fun <C : AbstractContact> getSingleReceipt( + result: Collection<MessageReceipt<*>>, + message: Message + ): MessageReceipt<C> { + when (result.size) { + 0 -> throw contextualBugReportException( + "Internal error: no MessageReceipt was returned from OutgoingMessagePipeline for message", + forDebug = message.structureToString() + ) + 1 -> return result.single().castUp() + else -> throw contextualBugReportException( + "Internal error: multiple MessageReceipts were returned from OutgoingMessagePipeline: $result", + forDebug = message.structureToString() + ) + } + } + + private fun <C : AbstractContact> createAttributesForOutgoingMessage( + target: C, + message: Message, + context: ComponentStorage + ): MutableTypeSafeMap { + val attributes = buildTypeSafeMap { + set(OutgoingMessagePipelineContext.CONTACT, target.impl()) + set(OutgoingMessagePipelineContext.ORIGINAL_MESSAGE, message) + set(OutgoingMessagePipelineContext.STEP, SendMessageStep.FIRST) + set(OutgoingMessagePipelineContext.PROTOCOL_STRATEGY, context[MessageProtocolStrategy].castUp()) + set(OutgoingMessagePipelineContext.HIGHWAY_UPLOADER, context[HighwayUploader]) + } + return attributes + } } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/OutgoingMessageContext.kt b/mirai-core/src/commonMain/kotlin/message/protocol/OutgoingMessageContext.kt deleted file mode 100644 index dceef13f5..000000000 --- a/mirai-core/src/commonMain/kotlin/message/protocol/OutgoingMessageContext.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2019-2022 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/dev/LICENSE - */ - -package net.mamoe.mirai.internal.message.protocol - -import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody - -internal interface OutgoingMessageContext { - - fun collect(elem: ImMsgBody.Elem) - - fun processAlso() -} - diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoder.kt b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoder.kt index 513b32f8a..c92e5d529 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoder.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoder.kt @@ -24,6 +24,8 @@ internal interface MessageDecoder : PipelineConsumptionMarker { internal class MessageDecoderProcessor( private val decoder: MessageDecoder, ) : Processor<MessageDecoderContext, ImMsgBody.Elem> { + override val origin: Any get() = this + override suspend fun process(context: MessageDecoderContext, data: ImMsgBody.Elem) { @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") decoder.run { context.process(data) } diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt index 68caa3b7c..6acec0407 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/decode/MessageDecoderPipeline.kt @@ -20,7 +20,8 @@ import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.utils.* import kotlin.coroutines.RestrictsSuspension -internal interface MessageDecoderPipeline : ProcessorPipeline<MessageDecoderProcessor, ImMsgBody.Elem, Message> +internal interface MessageDecoderPipeline : + ProcessorPipeline<MessageDecoderProcessor, MessageDecoderContext, ImMsgBody.Elem, Message> @RestrictsSuspension // Implementor can only call `MessageDecoderContext.process` and `processAlso` so there will be no suspension point internal interface MessageDecoderContext : ProcessorPipelineContext<ImMsgBody.Elem, Message> { @@ -41,7 +42,8 @@ internal open class MessageDecoderPipelineImpl : inner class MessageDecoderContextImpl(attributes: TypeSafeMap) : MessageDecoderContext, BaseContextImpl(attributes) - override fun createContext(attributes: TypeSafeMap): MessageDecoderContext = MessageDecoderContextImpl(attributes) + override fun createContext(data: ImMsgBody.Elem, attributes: TypeSafeMap): MessageDecoderContext = + MessageDecoderContextImpl(attributes) companion object { @TestOnly diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoder.kt b/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoder.kt index 3c9bf5eeb..f94e40e54 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoder.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoder.kt @@ -27,6 +27,8 @@ internal class MessageEncoderProcessor<T : SingleMessage>( private val encoder: MessageEncoder<T>, private val elementType: KClass<T>, ) : Processor<MessageEncoderContext, SingleMessage> { + override val origin: Any get() = this + override suspend fun process(context: MessageEncoderContext, data: SingleMessage) { if (elementType.isInstance(data)) { @Suppress("ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL") diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoderPipeline.kt b/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoderPipeline.kt index f294ebc7b..000318d25 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoderPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/encode/MessageEncoderPipeline.kt @@ -21,7 +21,7 @@ import net.mamoe.mirai.utils.* import kotlin.coroutines.RestrictsSuspension internal interface MessageEncoderPipeline : - ProcessorPipeline<MessageEncoderProcessor<*>, SingleMessage, ImMsgBody.Elem> { + ProcessorPipeline<MessageEncoderProcessor<*>, MessageEncoderContext, SingleMessage, ImMsgBody.Elem> { } @RestrictsSuspension @@ -73,7 +73,8 @@ internal open class MessageEncoderPipelineImpl : } } - override fun createContext(attributes: TypeSafeMap): MessageEncoderContext = MessageEncoderContextImpl(attributes) + override fun createContext(data: SingleMessage, attributes: TypeSafeMap): MessageEncoderContext = + MessageEncoderContextImpl(attributes) companion object { private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes() diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/FileMessageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/FileMessageProtocol.kt index 01e23fba1..9eb790be2 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/FileMessageProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/FileMessageProtocol.kt @@ -9,21 +9,92 @@ package net.mamoe.mirai.internal.message.protocol.impl +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.io.core.readUShort +import net.mamoe.mirai.internal.contact.SendMessageStep import net.mamoe.mirai.internal.message.data.FileMessageImpl +import net.mamoe.mirai.internal.message.data.checkIsImpl import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.ProcessorCollector import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoder import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoderContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.PROTOCOL_STRATEGY +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageSender +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageTransformer +import net.mamoe.mirai.internal.message.source.createMessageReceipt import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.ObjMsg +import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf +import net.mamoe.mirai.message.data.FileMessage +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor +import net.mamoe.mirai.message.data.visitor.acceptChildren import net.mamoe.mirai.utils.read +import net.mamoe.mirai.utils.systemProp internal class FileMessageProtocol : MessageProtocol() { override fun ProcessorCollector.collectProcessorsImpl() { // no encoder add(Decoder()) + + add(OutgoingMessageTransformer { + if (attributes[OutgoingMessagePipelineContext.STEP] == SendMessageStep.FIRST + ) { + verifyFileMessage(currentMessageChain) + } + }) + + add(FileMessageSender()) + } + + companion object { + private val ALLOW_SENDING_FILE_MESSAGE = systemProp("mirai.message.allow.sending.file.message", false) + + fun verifyFileMessage(message: MessageChain) { + message.acceptChildren(object : RecursiveMessageVisitor<Unit>() { + override fun visitFileMessage(message: FileMessage, data: Unit) { + if (ALLOW_SENDING_FILE_MESSAGE) return + // #1715 + if (message !is FileMessageImpl) error("Customized FileMessage cannot be send") + if (!message.allowSend) error( + "Sending FileMessage is not allowed, as it may cause unexpected results. " + + "Add JVM argument `-Dmirai.message.allow.sending.file.message=true` to disable this check. " + + "Do this only for compatibility!" + ) + } + }) + + } + } + + private class FileMessageSender : OutgoingMessageSender { + override suspend fun OutgoingMessagePipelineContext.process() { + val file = currentMessageChain[FileMessage] ?: return + markAsConsumed() + + file.checkIsImpl() + + val contact = attributes[CONTACT] + val bot = contact.bot + + val strategy = attributes[PROTOCOL_STRATEGY] + + val source = coroutineScope { + val source = async { + strategy.constructSourceForSpecialMessage(currentMessageChain, 2021) + } + + bot.network.sendAndExpect(FileManagement.Feed(bot.client, contact.id, file.busId, file.id)) + + source.await() + } + + collect(source.createMessageReceipt(contact, true)) + } } private class Decoder : MessageDecoder { diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ForwardMessageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ForwardMessageProtocol.kt new file mode 100644 index 000000000..f705ced46 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ForwardMessageProtocol.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.impl + +import net.mamoe.mirai.contact.MessageTooLargeException +import net.mamoe.mirai.internal.message.data.forwardMessage +import net.mamoe.mirai.internal.message.flags.IgnoreLengthCheck +import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.ProcessorCollector +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.HIGHWAY_UPLOADER +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.PROTOCOL_STRATEGY +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePreprocessor +import net.mamoe.mirai.message.data.ForwardMessage +import net.mamoe.mirai.message.data.RichMessage +import net.mamoe.mirai.message.data.toMessageChain +import net.mamoe.mirai.utils.currentTimeSeconds + +internal class ForwardMessageProtocol : MessageProtocol() { + override fun ProcessorCollector.collectProcessorsImpl() { + add(ForwardMessageUploader()) + } + + class ForwardMessageUploader : OutgoingMessagePreprocessor { + override suspend fun OutgoingMessagePipelineContext.process() { + val forward = currentMessageChain[ForwardMessage] ?: return + + val contact = attributes[CONTACT] + if (!currentMessageChain.contains(IgnoreLengthCheck)) { + check(forward.nodeList.size <= 200) { + throw MessageTooLargeException( + contact, forward, forward, + "ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}" + ) + } + sequence { + forward.nodeList.forEach { yieldAll(it.messageChain) } + }.asIterable().verifyLength(forward, contact) + } + + val resId = attributes[HIGHWAY_UPLOADER].uploadMessages( + contact, + attributes[PROTOCOL_STRATEGY], + forward.nodeList, + false + ) + + currentMessageChain = RichMessage.forwardMessage( + resId = resId, + fileName = currentTimeSeconds().toString(), + forwardMessage = forward, + ).toMessageChain() + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/GeneralMessageSenderProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/GeneralMessageSenderProtocol.kt new file mode 100644 index 000000000..7a4439d24 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/GeneralMessageSenderProtocol.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.impl + +import kotlinx.coroutines.Deferred +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.internal.AbstractBot +import net.mamoe.mirai.internal.contact.SendMessageStep +import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.ProcessorCollector +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.PROTOCOL_STRATEGY +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.STEP +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageSender +import net.mamoe.mirai.internal.message.source.createMessageReceipt +import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg +import net.mamoe.mirai.message.data.AtAll +import net.mamoe.mirai.message.data.OnlineMessageSource +import net.mamoe.mirai.message.data.content +import net.mamoe.mirai.message.data.toMessageChain +import net.mamoe.mirai.utils.buildTypeSafeMap +import net.mamoe.mirai.utils.truncated + +internal class GeneralMessageSenderProtocol : MessageProtocol(PRIORITY_GENERAL_SENDER) { + override fun ProcessorCollector.collectProcessorsImpl() { + add(GeneralMessageSender()) + } + + + class GeneralMessageSender : OutgoingMessageSender { + override suspend fun OutgoingMessagePipelineContext.process() { + markAsConsumed() + + val strategy = attributes[PROTOCOL_STRATEGY] + val step = attributes[STEP] + val contact = attributes[CONTACT] + val bot = contact.bot + + var source: Deferred<OnlineMessageSource.Outgoing>? = null + + val packets = strategy.createPacketsForGeneralMessage( + client = bot.client, + contact = contact, + message = currentMessageChain, + fragmented = step == SendMessageStep.FRAGMENTED, + sourceCallback = { source = it } + ) + + sendAllPackets(bot, step, contact, packets) + + val sourceAwait = source?.await() ?: error("Internal error: source is not initialized") + sourceAwait.tryEnsureSequenceIdAvailable() + collect(sourceAwait.createMessageReceipt(contact, true)) + } + + private suspend fun OutgoingMessagePipelineContext.sendAllPackets( + bot: AbstractBot, + step: SendMessageStep, + contact: Contact, + packets: List<OutgoingPacket> + ) = packets.forEach { packet -> + val originalMessage = attributes[OutgoingMessagePipelineContext.ORIGINAL_MESSAGE] + val finalMessage = currentMessageChain + + val resp = bot.network.sendAndExpect(packet) as MessageSvcPbSendMsg.Response + if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) { + val next = step.nextStepOrNull() + ?: throw MessageTooLargeException( + contact, + originalMessage, + finalMessage, + "Message '${finalMessage.content.truncated(10)}' is too large." + ) + + // retry with next step + processAlso( + originalMessage.toMessageChain(), + extraAttributes = buildTypeSafeMap { + set(STEP, next) + }, + ) // We expect to get a Receipt from processAlso + return@forEach + } + if (resp is MessageSvcPbSendMsg.Response.ServiceUnavailable) { + throw IllegalStateException("Send message to $contact failed, server service is unavailable.") + } + if (resp is MessageSvcPbSendMsg.Response.Failed) { + when (resp.resultType) { + 120 -> if (contact is Group) throw BotIsBeingMutedException(contact, originalMessage) + 121 -> if (AtAll in currentMessageChain) throw SendMessageFailedException( + contact, + SendMessageFailedException.Reason.AT_ALL_LIMITED, + originalMessage + ) + 299 -> if (contact is Group) throw SendMessageFailedException( + contact, + SendMessageFailedException.Reason.GROUP_CHAT_LIMITED, + originalMessage + ) + } + } + check(resp is MessageSvcPbSendMsg.Response.SUCCESS) { + "Send message failed: $resp" + } + + } + + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt index 2e07ff2e8..af3f3ccaf 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/ImageProtocol.kt @@ -10,6 +10,9 @@ package net.mamoe.mirai.internal.message.protocol.impl import net.mamoe.mirai.contact.User +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.message.data.transform import net.mamoe.mirai.internal.message.image.* import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.ProcessorCollector @@ -18,8 +21,12 @@ import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoderContext import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoder import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext.Companion.contact +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePreprocessor import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFace import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.utils.ImagePatcher import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.message.data.ImageType import net.mamoe.mirai.message.data.ShowImageFlag @@ -30,6 +37,29 @@ internal class ImageProtocol : MessageProtocol() { override fun ProcessorCollector.collectProcessorsImpl() { add(ImageEncoder()) add(ImageDecoder()) + + add(ImagePatcherForGroup()) + } + + private class ImagePatcherForGroup : OutgoingMessagePreprocessor { + override suspend fun OutgoingMessagePipelineContext.process() { + val contact = attributes[CONTACT] + if (contact !is GroupImpl) return + + val patcher = contact.bot.asQQAndroidBot().components[ImagePatcher] + currentMessageChain = currentMessageChain.transform { element -> + when (element) { + is OfflineGroupImage -> { + patcher.patchOfflineGroupImage(contact, element) + element + } + is FriendImage -> { + patcher.patchFriendImageToGroupImage(contact, element) + } + else -> element + } + } + } } private class ImageDecoder : MessageDecoder { diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/LongMessageProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/LongMessageProtocol.kt new file mode 100644 index 000000000..7fff3f90f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/LongMessageProtocol.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.impl + +import net.mamoe.mirai.internal.contact.AbstractContact +import net.mamoe.mirai.internal.contact.SendMessageStep +import net.mamoe.mirai.internal.contact.takeContent +import net.mamoe.mirai.internal.message.data.longMessage +import net.mamoe.mirai.internal.message.flags.DontAsLongMessage +import net.mamoe.mirai.internal.message.flags.ForceAsLongMessage +import net.mamoe.mirai.internal.message.flags.IgnoreLengthCheck +import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.ProcessorCollector +import net.mamoe.mirai.internal.message.protocol.outgoing.MessageProtocolStrategy +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.HIGHWAY_UPLOADER +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.PROTOCOL_STRATEGY +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.STEP +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageTransformer +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.RichMessage +import net.mamoe.mirai.utils.currentTimeSeconds + +internal class LongMessageProtocol : MessageProtocol() { + override fun ProcessorCollector.collectProcessorsImpl() { + add(OutgoingMessageTransformer { + currentMessageChain = + convertToLongMessageIfNeeded( + currentMessageChain, + attributes[STEP], + attributes[CONTACT], + attributes[PROTOCOL_STRATEGY] + ) + }) + } + + private suspend fun OutgoingMessagePipelineContext.convertToLongMessageIfNeeded( + chain: MessageChain, + step: SendMessageStep, + contact: AbstractContact, + strategy: MessageProtocolStrategy<*> + ): MessageChain { + val uploader = attributes[HIGHWAY_UPLOADER] + + suspend fun sendLongImpl(): MessageChain { + val time = currentTimeSeconds() + val resId = uploader.uploadLongMessage(contact, strategy, chain, time.toInt()) + return chain + RichMessage.longMessage( + brief = chain.takeContent(27), + resId = resId, + timeSeconds = time + ) // LongMessageInternal replaces all contents and preserves metadata + } + + return when (step) { + SendMessageStep.FIRST -> { + // 只需要在第一次发送的时候验证长度 + // 后续重试直接跳过 + if (chain.contains(ForceAsLongMessage)) { + return sendLongImpl() + } + + if (!chain.contains(IgnoreLengthCheck)) { + chain.verifyLength(chain, contact) + } + + chain + } + SendMessageStep.LONG_MESSAGE -> { + if (chain.contains(DontAsLongMessage)) chain // fragmented + else sendLongImpl() + } + SendMessageStep.FRAGMENTED -> chain + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt index 418f1286e..e91c26ebf 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MusicShareProtocol.kt @@ -9,10 +9,17 @@ package net.mamoe.mirai.internal.message.protocol.impl +import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.ProcessorCollector import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoder import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext.Companion.CONTACT +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessageSender +import net.mamoe.mirai.internal.message.source.createMessageReceipt +import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket +import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.MusicShare import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.content @@ -22,6 +29,8 @@ internal class MusicShareProtocol : MessageProtocol() { add(Encoder()) // no decoder. refined from LightApp // add(Decoder()) + + add(Sender()) } private class Encoder : MessageEncoder<MusicShare> { @@ -32,4 +41,25 @@ internal class MusicShareProtocol : MessageProtocol() { processAlso(PlainText(data.content)) } } + + private class Sender : OutgoingMessageSender { + override suspend fun OutgoingMessagePipelineContext.process() { + val contact = attributes[CONTACT] + val bot = contact.bot + val musicShare = currentMessageChain[MusicShare] ?: return + + val packet = MusicSharePacket( + bot.client, musicShare, contact.id, + targetKind = if (contact is Group) MessageSourceKind.GROUP else MessageSourceKind.FRIEND // always FRIEND + ) + val result = bot.network.sendAndExpect(packet) + result.pkg.checkSuccess("send music share") + + val strategy = attributes[OutgoingMessagePipelineContext.PROTOCOL_STRATEGY] + val source = strategy.constructSourceForSpecialMessage(currentMessageChain, 3116) + source.tryEnsureSequenceIdAvailable() + + collect(source.createMessageReceipt(contact, true)) + } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt index 778fe5a16..1b8c06608 100644 --- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt +++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/QuoteReplyProtocol.kt @@ -21,8 +21,10 @@ import net.mamoe.mirai.internal.message.protocol.decode.MessageDecoderContext.Co import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoder import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext import net.mamoe.mirai.internal.message.protocol.encode.MessageEncoderContext.Companion.contact +import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePreprocessor import net.mamoe.mirai.internal.message.source.MessageSourceInternal import net.mamoe.mirai.internal.message.source.OfflineMessageSourceImplData +import net.mamoe.mirai.internal.message.source.ensureSequenceIdAvailable import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.OnlineMessageSource @@ -32,6 +34,10 @@ internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) { override fun ProcessorCollector.collectProcessorsImpl() { add(Encoder()) add(Decoder()) + + add(OutgoingMessagePreprocessor { + currentMessageChain[QuoteReply]?.source?.ensureSequenceIdAvailable() + }) } private class Decoder : MessageDecoder { diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/HighwayUploader.kt b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/HighwayUploader.kt new file mode 100644 index 000000000..5b378935a --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/HighwayUploader.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.outgoing + +import net.mamoe.mirai.internal.contact.AbstractContact +import net.mamoe.mirai.internal.contact.nickIn +import net.mamoe.mirai.internal.message.data.MultiMsgUploader +import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade +import net.mamoe.mirai.internal.message.source.ensureSequenceIdAvailable +import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.message.data.ForwardMessage +import net.mamoe.mirai.message.data.MessageChain + +internal interface HighwayUploader { + suspend fun uploadMessages( + contact: AbstractContact, + strategy: MessageProtocolStrategy<*>, + nodes: Collection<ForwardMessage.INode>, + isLong: Boolean, + facade: MessageProtocolFacade = MessageProtocolFacade, + senderName: String = contact.bot.nickIn(contact), + ): String { + nodes.forEach { it.messageChain.ensureSequenceIdAvailable() } + + val uploader = MultiMsgUploader( + client = contact.bot.client, + isLong = isLong, + facade = facade, + contact = contact, + senderName = senderName, + strategy = strategy + ).also { it.emitMain(nodes) } + + return uploader.uploadAndReturnResId() + } + + suspend fun uploadLongMessage( + contact: AbstractContact, + strategy: MessageProtocolStrategy<*>, + chain: MessageChain, + timeSeconds: Int, + senderName: String = contact.bot.nickIn(contact), + ): String { + val bot = contact.bot + return uploadMessages( + contact, + strategy, + listOf( + ForwardMessage.Node( + senderId = bot.id, + time = timeSeconds, + messageChain = chain, + senderName = senderName + ) + ), + true, + senderName = senderName + ) + } + + companion object : ComponentKey<HighwayUploader> + + object Default : HighwayUploader +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/MessageProtocolStrategy.kt b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/MessageProtocolStrategy.kt new file mode 100644 index 000000000..1e5c5fe57 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/MessageProtocolStrategy.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.outgoing + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.withTimeoutOrNull +import net.mamoe.mirai.event.EventPriority +import net.mamoe.mirai.event.GlobalEventChannel +import net.mamoe.mirai.event.nextEvent +import net.mamoe.mirai.internal.contact.* +import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl +import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToGroupImpl +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.internal.network.components.ClockHolder.Companion.clock +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor +import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor +import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.* +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.OnlineMessageSource + +internal sealed interface MessageProtocolStrategy<in C : AbstractContact> { + + suspend fun createPacketsForGeneralMessage( + client: QQAndroidClient, + contact: C, + message: MessageChain, + fragmented: Boolean, + sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit, + ): List<OutgoingPacket> + + suspend fun constructSourceForSpecialMessage( + finalMessage: MessageChain, + fromAppId: Int, + ): OnlineMessageSource.Outgoing + + companion object : ComponentKey<MessageProtocolStrategy<*>> +} + +internal sealed class UserMessageProtocolStrategy<C : AbstractUser> : MessageProtocolStrategy<C> { + override suspend fun constructSourceForSpecialMessage( + finalMessage: MessageChain, + fromAppId: Int + ): OnlineMessageSource.Outgoing { + throw UnsupportedOperationException("Sending MusicShare or FileMessage to User is not yet supported") + } +} + +internal class FriendMessageProtocolStrategy( + private val contact: FriendImpl, +) : UserMessageProtocolStrategy<FriendImpl>() { + override suspend fun createPacketsForGeneralMessage( + client: QQAndroidClient, + contact: FriendImpl, + message: MessageChain, + fragmented: Boolean, + sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit + ): List<OutgoingPacket> { + return MessageSvcPbSendMsg.createToFriend(client, contact, message, fragmented, sourceCallback) + } + + override suspend fun constructSourceForSpecialMessage( + finalMessage: MessageChain, + fromAppId: Int + ): OnlineMessageSource.Outgoing { + val receipt: PrivateMessageProcessor.SendPrivateMessageReceipt = withTimeoutOrNull(3000) { + GlobalEventChannel.parentScope(this).nextEvent(EventPriority.MONITOR) { + it.bot === contact.bot && it.fromAppId == fromAppId + } + } ?: PrivateMessageProcessor.SendPrivateMessageReceipt.EMPTY + + return OnlineMessageSourceToFriendImpl( + internalIds = intArrayOf(receipt.messageRandom), + sequenceIds = intArrayOf(receipt.sequenceId), + sender = contact.bot, + target = contact, + time = contact.bot.clock.server.currentTimeSeconds().toInt(), + originalMessage = finalMessage + ) + } +} + +internal object StrangerMessageProtocolStrategy : UserMessageProtocolStrategy<StrangerImpl>() { + override suspend fun createPacketsForGeneralMessage( + client: QQAndroidClient, + contact: StrangerImpl, + message: MessageChain, + fragmented: Boolean, + sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit + ): List<OutgoingPacket> { + return MessageSvcPbSendMsg.createToStranger(client, contact, message, fragmented, sourceCallback) + } +} + +internal object GroupTempMessageProtocolStrategy : UserMessageProtocolStrategy<NormalMemberImpl>() { + override suspend fun createPacketsForGeneralMessage( + client: QQAndroidClient, + contact: NormalMemberImpl, + message: MessageChain, + fragmented: Boolean, + sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit + ): List<OutgoingPacket> { + return MessageSvcPbSendMsg.createToTemp(client, contact, message, fragmented, sourceCallback) + } +} + +internal open class GroupMessageProtocolStrategy( + private val contact: GroupImpl, +) : MessageProtocolStrategy<GroupImpl> { + override suspend fun createPacketsForGeneralMessage( + client: QQAndroidClient, + contact: GroupImpl, + message: MessageChain, + fragmented: Boolean, + sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit + ): List<OutgoingPacket> { + return MessageSvcPbSendMsg.createToGroup(client, contact, message, fragmented, sourceCallback) + } + + override suspend fun constructSourceForSpecialMessage( + finalMessage: MessageChain, + fromAppId: Int + ): OnlineMessageSource.Outgoing { + val receipt: GroupMessageProcessor.SendGroupMessageReceipt = withTimeoutOrNull(3000) { + GlobalEventChannel.parentScope(this).nextEvent(EventPriority.MONITOR) { + it.bot === contact.bot && it.fromAppId == fromAppId + } + } ?: GroupMessageProcessor.SendGroupMessageReceipt.EMPTY + + return OnlineMessageSourceToGroupImpl( + contact, + internalIds = intArrayOf(receipt.messageRandom), + providedSequenceIds = intArrayOf(receipt.sequenceId), + sender = contact.bot, + target = contact, + time = contact.bot.clock.server.currentTimeSeconds().toInt(), + originalMessage = finalMessage + ) + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipeline.kt b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipeline.kt new file mode 100644 index 000000000..0850e8efd --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipeline.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.outgoing + +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.MessageTooLargeException +import net.mamoe.mirai.internal.contact.AbstractContact +import net.mamoe.mirai.internal.contact.SendMessageStep +import net.mamoe.mirai.internal.message.source.ensureSequenceIdAvailable +import net.mamoe.mirai.internal.network.handler.logger +import net.mamoe.mirai.internal.pipeline.AbstractProcessorPipeline +import net.mamoe.mirai.internal.pipeline.PipelineConfiguration +import net.mamoe.mirai.internal.pipeline.ProcessorPipeline +import net.mamoe.mirai.internal.pipeline.ProcessorPipelineContext +import net.mamoe.mirai.internal.utils.estimateLength +import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.* + +/////////////////////////////////////////////////////////////////////////// +// Infrastructure for a ProcessorPipeline +/////////////////////////////////////////////////////////////////////////// + +// Just to change this easily — you'd know it's actually Unit — a placeholder. +internal typealias OutgoingMessagePipelineInput = MessageChain + + +internal interface OutgoingMessagePipeline : + ProcessorPipeline<OutgoingMessagePipelineProcessor, OutgoingMessagePipelineContext, OutgoingMessagePipelineInput, MessageReceipt<*>> + +internal open class OutgoingMessagePipelineImpl : + AbstractProcessorPipeline<OutgoingMessagePipelineProcessor, OutgoingMessagePipelineContext, OutgoingMessagePipelineInput, MessageReceipt<*>>( + PipelineConfiguration(stopWhenConsumed = false), @OptIn(TestOnly::class) defaultTraceLogging + ), OutgoingMessagePipeline { + + inner class OutgoingMessagePipelineContextImpl( + attributes: TypeSafeMap, override var currentMessageChain: MessageChain + ) : OutgoingMessagePipelineContext, BaseContextImpl(attributes) + + override fun createContext( + data: OutgoingMessagePipelineInput, attributes: TypeSafeMap + ): OutgoingMessagePipelineContext = OutgoingMessagePipelineContextImpl(attributes, data) + + companion object { + @TestOnly + val defaultTraceLogging: MiraiLoggerWithSwitch by lazy { + MiraiLogger.Factory.create(OutgoingMessagePipelineImpl::class, "OutgoingMessagePipeline") + .withSwitch(systemProp("mirai.message.outgoing.pipeline.log.full", false)) + } + } +} + + +internal interface OutgoingMessagePipelineContext : + ProcessorPipelineContext<OutgoingMessagePipelineInput, MessageReceipt<*>> { + var currentMessageChain: MessageChain + + suspend fun MessageSource.tryEnsureSequenceIdAvailable() { + val contact = attributes[CONTACT] + val bot = contact.bot + try { + ensureSequenceIdAvailable() + } catch (e: Exception) { + bot.network.logger.warning( + "Timeout awaiting sequenceId for message(${currentMessageChain.content.take(10)}). Some features may not work properly.", + e + ) + } + } + + fun Iterable<SingleMessage>.countImages(): Int = this.count { it is Image } + + fun Iterable<SingleMessage>.verifyLength( + originalMessage: Message, target: Contact, + ): Int { + val chain = this + val length = estimateLength(target, 15001) + if (length > 15000 || countImages() > 50) { + throw MessageTooLargeException( + target, originalMessage, this.toMessageChain(), + "message(${ + chain.joinToString("", limit = 10).let { rsp -> + if (rsp.length > 100) { + rsp.take(100) + "..." + } else rsp + } + }) is too large. Allow up to 50 images or 5000 chars" + ) + } + return length + } + + + companion object { + /** + * Original + */ + val ORIGINAL_MESSAGE = TypeKey<Message>("originalMessage") + + + /** + * Message target + */ + val CONTACT = TypeKey<AbstractContact>("contact") + + val STEP = TypeKey<SendMessageStep>("step") + + val PROTOCOL_STRATEGY = TypeKey<MessageProtocolStrategy<AbstractContact>>("protocolStrategy") + + val HIGHWAY_UPLOADER = TypeKey<HighwayUploader>("highwayUploader") + } +} + diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipelineProcessor.kt b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipelineProcessor.kt new file mode 100644 index 000000000..fb801c2e6 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessagePipelineProcessor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.outgoing + +import net.mamoe.mirai.internal.pipeline.Processor + +internal sealed interface OutgoingMessagePipelineProcessor : + Processor<OutgoingMessagePipelineContext, OutgoingMessagePipelineInput> + +/** + * Adapter for [OutgoingMessageProcessor] to be used as [Processor]. + */ +internal class OutgoingMessageProcessorAdapter( + private val processor: OutgoingMessageProcessor, +) : OutgoingMessagePipelineProcessor { + override val origin: OutgoingMessageProcessor get() = processor + + override suspend fun process(context: OutgoingMessagePipelineContext, data: OutgoingMessagePipelineInput) { + processor.run { context.process() } + } + + override fun toString(): String { + return "OutgoingMessageProcessorAdapter(transformer=$processor)" + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessageProcessor.kt b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessageProcessor.kt new file mode 100644 index 000000000..8dee9e8be --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/protocol/outgoing/OutgoingMessageProcessor.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.outgoing + +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.internal.message.data.LongMessageInternal +import net.mamoe.mirai.internal.pipeline.PipelineConsumptionMarker +import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.MessageChain + +/** + * A handler that is responsible for some logic in sending a [MessageChain] to a target [Contact]. + * + * Broadcasting events is not of responsibility of [OutgoingMessageProcessor]. + */ +internal sealed interface OutgoingMessageProcessor { + suspend fun OutgoingMessagePipelineContext.process() +} + +/** + * A preprocessor will be called in the first place. + * + * It is designed to do type conversions, and pre-conditional checks. + * + * **Preprocessors are not called in nested processing.** + */ +internal fun interface OutgoingMessagePreprocessor : OutgoingMessageProcessor + + +/** + * A transformer will be called after [Preprocessors][OutgoingMessagePreprocessor] and before [Postprocessors][OutgoingMessagePostprocessor]. + * + * It is capable for handling some special messages, e.g. transform long messages as [LongMessageInternal]. + */ +internal fun interface OutgoingMessageTransformer : OutgoingMessageProcessor + +/** + * A transformer will be called after [Preprocessors][OutgoingMessagePreprocessor] and before [Postprocessors][OutgoingMessagePostprocessor]. + * + * It is capable for sending packets, and create [MessageReceipt]. + * Senders can finish the pipeline by [OutgoingMessagePipelineContext.markAsConsumed]. + */ +internal fun interface OutgoingMessageSender : OutgoingMessageProcessor, PipelineConsumptionMarker + +/** + * A postprocessor will be called in the last place. + * + * It is designed to do cleanup. + * + * Note that if an exception was thrown by previous processors, postprocessors may not be called, + * so do not reply on the postprocessor to close resources. + */ +internal fun interface OutgoingMessagePostprocessor : OutgoingMessageProcessor + +//internal interface MessageRefiner +// +//internal fun interface DeepMessageRefiner : MessageRefiner, OutgoingMessageProcessor +// +//@RestrictsSuspension // only allowed to call `processAlso` +//internal fun interface LightMessageRefiner : MessageRefiner { +// suspend fun OutgoingMessagePipelineContext.process() +//} diff --git a/mirai-core/src/commonMain/kotlin/message/source/MessageSourceInternal.kt b/mirai-core/src/commonMain/kotlin/message/source/MessageSourceInternal.kt index f6fd4313e..d0070d89a 100644 --- a/mirai-core/src/commonMain/kotlin/message/source/MessageSourceInternal.kt +++ b/mirai-core/src/commonMain/kotlin/message/source/MessageSourceInternal.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.internal.message.source import kotlinx.serialization.Transient import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.internal.contact.SendMessageHandler import net.mamoe.mirai.internal.message.LightMessageRefiner.dropMiraiInternalFlags import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight import net.mamoe.mirai.internal.message.visitor.ex @@ -60,8 +59,6 @@ internal interface OutgoingMessageSourceInternal : MessageSourceInternal { /** * This 'overrides' [MessageSource.originalMessage]. - * - * @see SendMessageHandler.sendMessagePacket */ var originalMessage: MessageChain } diff --git a/mirai-core/src/commonMain/kotlin/network/component/ComponentStorage.kt b/mirai-core/src/commonMain/kotlin/network/component/ComponentStorage.kt index b0d727a25..fe90fd8ce 100644 --- a/mirai-core/src/commonMain/kotlin/network/component/ComponentStorage.kt +++ b/mirai-core/src/commonMain/kotlin/network/component/ComponentStorage.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 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. + * 此源代码的使用受 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 + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.internal.network.component @@ -37,6 +37,10 @@ internal interface ComponentStorage { } } +internal fun buildComponentStorage(builderAction: MutableComponentStorage.() -> Unit): ComponentStorage { + return ConcurrentComponentStorage(builderAction) +} + internal fun ComponentStorage?.withFallback(fallback: ComponentStorage?): ComponentStorage { if (this == null) return fallback ?: return ComponentStorage.EMPTY if (fallback == null) return this diff --git a/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt b/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt index 37ee8bf7c..59b2ead50 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt @@ -37,7 +37,8 @@ import kotlin.reflect.KClass /** * Centralized processor pipeline for [MessageSvcPbGetMsg] and [OnlinePushPbPushTransMsg] */ -internal interface NoticeProcessorPipeline : ProcessorPipeline<NoticeProcessor, ProtocolStruct, Packet> { +internal interface NoticeProcessorPipeline : + ProcessorPipeline<NoticeProcessor, NoticePipelineContext, ProtocolStruct, Packet> { companion object : ComponentKey<NoticeProcessorPipeline> { val ComponentStorage.noticeProcessorPipeline get() = get(NoticeProcessorPipeline) @@ -46,7 +47,7 @@ internal interface NoticeProcessorPipeline : ProcessorPipeline<NoticeProcessor, data: ProtocolStruct, attributes: TypeSafeMap = TypeSafeMap.EMPTY, ): Packet { - return components.noticeProcessorPipeline.process(data, attributes).toPacket() + return components.noticeProcessorPipeline.process(data, attributes).collected.toPacket() } } } @@ -93,10 +94,10 @@ internal open class NoticeProcessorPipelineImpl protected constructor( override suspend fun processAlso( data: ProtocolStruct, attributes: TypeSafeMap - ): Collection<Packet> { + ): ProcessResult<out ProcessorPipelineContext<ProtocolStruct, Packet>, Packet> { traceLogging.info { "processAlso: data=${data.structureToStringAndDesensitizeIfAvailable()}" } return process(data, this.attributes + attributes).also { packets -> - this.collected.data += packets + this.collected.data += packets.collected traceLogging.info { "processAlso: result=$packets" } } } @@ -120,7 +121,8 @@ internal open class NoticeProcessorPipelineImpl protected constructor( ) } - override fun createContext(attributes: TypeSafeMap): NoticePipelineContext = ContextImpl(attributes) + override fun createContext(data: ProtocolStruct, attributes: TypeSafeMap): NoticePipelineContext = + ContextImpl(attributes) protected open fun packetToString(data: Any?): String = data.toDebugString("mirai.network.notice.pipeline.log.full") diff --git a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt index 033c0ef81..590dbf2fb 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/NetworkHandler.kt @@ -147,7 +147,6 @@ internal interface NetworkHandler : CoroutineScope { */ suspend fun <P : Packet?> sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): P - /** * Sends [packet] and does not expect any response. * diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt index 503b78b37..9f995040b 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt @@ -13,29 +13,17 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.SendMessageHandler import net.mamoe.mirai.internal.message.contextualBugReportException -import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade -import net.mamoe.mirai.internal.message.source.MessageSourceInternal import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.components.PacketCodec -import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit import net.mamoe.mirai.internal.network.protocol.data.proto.MultiMsg import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf -import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.internal.utils.structureToString -import net.mamoe.mirai.message.data.ForwardMessage -import net.mamoe.mirai.message.data.MessageSource -import net.mamoe.mirai.message.data.toMessageChain -import net.mamoe.mirai.utils.gzip import net.mamoe.mirai.utils.md5 -import net.mamoe.mirai.utils.toLongUnsigned internal class MessageValidationData( val data: ByteArray, @@ -46,71 +34,6 @@ internal class MessageValidationData( } } -internal fun Collection<ForwardMessage.INode>.calculateValidationData( - client: QQAndroidClient, - random: Int, - handler: SendMessageHandler<*>, - isLong: Boolean, -): MessageValidationData { - val offeredSourceIds = mutableSetOf<Int>() - fun calculateMsgSeq(node: ForwardMessage.INode): Int { - node.messageChain[MessageSource]?.let { source -> - source as MessageSourceInternal - - val sid = source.sequenceIds.first() - // Duplicate message added - if (offeredSourceIds.add(sid)) { - return sid - } - } - return client.atomicNextMessageSequenceId() - } - - val msgList = map { chain -> - MsgComm.Msg( - msgHead = MsgComm.MsgHead( - fromUin = chain.senderId, - toUin = if (isLong) { - handler.targetUserUin ?: 0 - } else 0, - msgSeq = calculateMsgSeq(chain), - msgTime = chain.time, - msgUid = 0x01000000000000000L or random.toLongUnsigned(), - mutiltransHead = MsgComm.MutilTransHead( - status = 0, - msgId = 1 - ), - msgType = 82, // troop - groupInfo = handler.run { chain.groupInfo }, - isSrcMsg = false - ), - msgBody = ImMsgBody.MsgBody( - richText = ImMsgBody.RichText( - elems = MessageProtocolFacade.encode( - chain.messageChain.toMessageChain(), - handler.contact, - withGeneralFlags = false, - isForward = true - ) - ) - ) - ) - } - val msgTransmit = MsgTransmit.PbMultiMsgTransmit( - msg = msgList, - pbItemList = listOf( - MsgTransmit.PbMultiMsgItem( - fileName = "MultiMsg", - buffer = MsgTransmit.PbMultiMsgNew(msgList).toByteArray(MsgTransmit.PbMultiMsgNew.serializer()) - ) - ) - ) - - val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) - - return MessageValidationData(bytes.gzip()) -} - internal class MultiMsg { object ApplyUp : OutgoingPacketFactory<ApplyUp.Response>("MultiMsg.ApplyUp") { diff --git a/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt b/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt index c3f4ad857..48db5e7b7 100644 --- a/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt +++ b/mirai-core/src/commonMain/kotlin/pipeline/ProcessorPipeline.kt @@ -19,11 +19,13 @@ import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedQueue internal interface Processor<C : ProcessorPipelineContext<D, *>, D> : PipelineConsumptionMarker { + val origin: Any get() = this + suspend fun process(context: C, data: D) } -internal interface ProcessorPipeline<P : Processor<out ProcessorPipelineContext<D, *>, D>, D, R> { - val processors: Collection<P> +internal interface ProcessorPipeline<P : Processor<C, D>, C : ProcessorPipelineContext<D, R>, D, R> { + val processors: MutableCollection<ProcessorBox<P>> fun interface DisposableRegistry : Closeable { fun dispose() @@ -37,15 +39,51 @@ internal interface ProcessorPipeline<P : Processor<out ProcessorPipelineContext< fun registerBefore(processor: P): DisposableRegistry + + /** + * Process using the [context]. + */ + suspend fun process( + data: D, + context: C, + attributes: TypeSafeMap = TypeSafeMap.EMPTY, + ): ProcessResult<C, R> + + /** + * Process with a new context + */ suspend fun process( data: D, attributes: TypeSafeMap = TypeSafeMap.EMPTY - ): Collection<R> - + ): ProcessResult<C, R> } +internal inline fun <P : Processor<*, *>, Pip : ProcessorPipeline<P, *, *, *>> Pip.replaceProcessor( + predicate: (origin: Any) -> Boolean, + processor: P +): Boolean { + for (box in processors) { + val value = box.value + if (predicate(value.origin)) { + box.value = processor + return true + } + } + return false +} + + +internal data class ProcessorBox<P : Processor<*, *>>( + var value: P +) + +internal data class ProcessResult<C : ProcessorPipelineContext<*, R>, R>( + val context: C, + val collected: Collection<R>, +) + @JvmInline -internal value class MutableProcessResult<R>( +internal value class MutablePipelineResult<R>( val data: MutableCollection<R> ) @@ -59,10 +97,10 @@ internal interface ProcessorPipelineContext<D, R> { */ val attributes: TypeSafeMap - val collected: MutableProcessResult<R> + val collected: MutablePipelineResult<R> // DSL to simplify some expressions - operator fun MutableProcessResult<R>.plusAssign(result: R?) { + operator fun MutablePipelineResult<R>.plusAssign(result: R?) { if (result != null) collect(result) } @@ -103,10 +141,13 @@ internal interface ProcessorPipelineContext<D, R> { /** * Fire the [data] into the processor pipeline, and collect the results to current [collected]. * - * @param attributes extra attributes + * @param extraAttributes extra attributes * @return result collected from processors. This would also have been collected to this context (where you call [processAlso]). */ - suspend fun processAlso(data: D, attributes: TypeSafeMap = TypeSafeMap.EMPTY): Collection<R> + suspend fun processAlso( + data: D, + extraAttributes: TypeSafeMap = TypeSafeMap.EMPTY + ): ProcessResult<out ProcessorPipelineContext<D, R>, R> } internal abstract class AbstractProcessorPipelineContext<D, R>( @@ -130,7 +171,7 @@ internal abstract class AbstractProcessorPipelineContext<D, R>( } } - override val collected: MutableProcessResult<R> = MutableProcessResult(ConcurrentLinkedQueue()) + override val collected: MutablePipelineResult<R> = MutablePipelineResult(ConcurrentLinkedQueue()) override fun collect(result: R) { collected.data.add(result) @@ -151,37 +192,42 @@ internal abstract class AbstractProcessorPipeline<P : Processor<C, D>, C : Proce protected constructor( val configuration: PipelineConfiguration, val traceLogging: MiraiLogger, -) : ProcessorPipeline<P, D, R> { +) : ProcessorPipeline<P, C, D, R> { constructor(configuration: PipelineConfiguration) : this(configuration, SilentLogger) /** * Must be ordered */ - override val processors = ConcurrentLinkedDeque<P>() + override val processors: ConcurrentLinkedDeque<ProcessorBox<P>> = ConcurrentLinkedDeque() override fun registerProcessor(processor: P): ProcessorPipeline.DisposableRegistry { - processors.add(processor) + val box = ProcessorBox(processor) + processors.add(box) return ProcessorPipeline.DisposableRegistry { - processors.remove(processor) + processors.remove(box) } } override fun registerBefore(processor: P): ProcessorPipeline.DisposableRegistry { - processors.addFirst(processor) + val box = ProcessorBox(processor) + processors.addFirst(box) return ProcessorPipeline.DisposableRegistry { - processors.remove(processor) + processors.remove(box) } } - protected abstract fun createContext(attributes: TypeSafeMap): C + protected abstract fun createContext(data: D, attributes: TypeSafeMap): C abstract inner class BaseContextImpl( attributes: TypeSafeMap, ) : AbstractProcessorPipelineContext<D, R>(attributes, traceLogging) { - override suspend fun processAlso(data: D, attributes: TypeSafeMap): Collection<R> { + override suspend fun processAlso( + data: D, + extraAttributes: TypeSafeMap + ): ProcessResult<out ProcessorPipelineContext<D, R>, R> { traceLogging.info { "processAlso: data=${data.structureToStringAndDesensitizeIfAvailable()}" } - return process(data, this.attributes + attributes).also { - this.collected.data += it + return process(data, this.attributes + extraAttributes).also { + this.collected.data += it.collected traceLogging.info { "processAlso: result=$it" } } } @@ -195,14 +241,17 @@ protected constructor( e: Throwable ): Unit = throw e - override suspend fun process(data: D, attributes: TypeSafeMap): Collection<R> { + override suspend fun process(data: D, attributes: TypeSafeMap): ProcessResult<C, R> { + return process(data, createContext(data, attributes), attributes) + } + + override suspend fun process(data: D, context: C, attributes: TypeSafeMap): ProcessResult<C, R> { traceLogging.info { "process: data=${data.structureToStringAndDesensitizeIfAvailable()}" } - val context = createContext(attributes) val diff = if (traceLogging.isEnabled) CollectionDiff<R>() else null diff?.save(context.collected.data) - for (processor in processors) { + for ((processor) in processors) { val result = kotlin.runCatching { processor.process(context, data) @@ -226,6 +275,6 @@ protected constructor( break } } - return context.collected.data + return ProcessResult(context, context.collected.data) } } diff --git a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol index 45c3e626b..d6be10ef0 100644 --- a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol +++ b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.internal.message.protocol.MessageProtocol @@ -21,4 +21,7 @@ net.mamoe.mirai.internal.message.protocol.impl.QuoteReplyProtocol net.mamoe.mirai.internal.message.protocol.impl.RichMessageProtocol net.mamoe.mirai.internal.message.protocol.impl.TextProtocol net.mamoe.mirai.internal.message.protocol.impl.VipFaceProtocol +net.mamoe.mirai.internal.message.protocol.impl.ForwardMessageProtocol +net.mamoe.mirai.internal.message.protocol.impl.LongMessageProtocol net.mamoe.mirai.internal.message.protocol.impl.UnsupportedMessageProtocol +net.mamoe.mirai.internal.message.protocol.impl.GeneralMessageSenderProtocol \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt index 71182262c..c91de162d 100644 --- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/AbstractMessageProtocolTest.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import net.mamoe.mirai.contact.ContactOrBot import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.internal.contact.inferMessageSourceKind +import net.mamoe.mirai.internal.message.data.inferMessageSourceKind import net.mamoe.mirai.internal.message.protocol.MessageProtocol import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacadeImpl diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt new file mode 100644 index 000000000..db705ce4b --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/QuoteReplyProtocolTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.message.protocol.impl + +import net.mamoe.mirai.internal.message.protocol.MessageProtocol +import net.mamoe.mirai.internal.message.protocol.decodeAndRefineLight +import net.mamoe.mirai.message.data.Face +import net.mamoe.mirai.message.data.MessageSourceKind +import net.mamoe.mirai.message.data.messageChainOf +import net.mamoe.mirai.utils.hexToBytes +import org.junit.jupiter.api.Test + +internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() { + override val protocols: Array<out MessageProtocol> = arrayOf(QuoteReplyProtocol(), TextProtocol()) + + @Test + fun `decode group reference group`() { + buildChecks { + elem( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + srcMsg = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.SourceMsg( + origSeqs = intArrayOf(1803), + senderUin = 1230001, + time = 1653147259, + flag = 1, + elems = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "a", + ), + ), + ), + pbReserve = "18 AB 85 9D 81 82 80 80 80 01".hexToBytes(), + ), + ), + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "s", + ), + ), + ) +// message( +// QuoteReply( +//// OfflineMessageSourceImplData( +//// +//// ) +// ) +// ) + targetGroup() + useOrdinaryEquality() + }.doDecoderChecks() + } + + @Test + fun `can decode`() { + doDecoderChecks( + messageChainOf(Face(Face.YIN_XIAN)), + ) { + decodeAndRefineLight( + listOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + face = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Face( + index = 108, + old = "14 AD".hexToBytes(), + ), + ) + ), + groupIdOrZero = 0, + MessageSourceKind.GROUP, + bot, + ) + } + + } + + private fun ChecksBuilder.targetGroup() { + target(bot.addGroup(1, 1)) + } + + private fun ChecksBuilder.targetFriend() { + target(bot.addFriend(1)) + } + + +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt index 2bb2b9d0f..4e01582c1 100644 --- a/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt +++ b/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt @@ -63,7 +63,7 @@ internal abstract class AbstractNoticeProcessorTest : AbstractNettyNHTest(), Gro bot.components[SsoProcessor].firstLoginResult.value = FirstLoginResult.PASSED val handler = LoggingPacketHandlerAdapter(PacketLoggingStrategyImpl(bot), bot.logger) val context = UseTestContext(attributes.toMutableTypeSafeMap()) - return pipeline.process(block(context), context.attributes).also { list -> + return pipeline.process(block(context), context.attributes).collected.also { list -> for (packet in list) { handler.handlePacket(IncomingPacket("test", packet)) } @@ -77,10 +77,10 @@ internal abstract class AbstractNoticeProcessorTest : AbstractNettyNHTest(), Gro ): Collection<Packet> = use(attributes, pipeline = object : NoticeProcessorPipelineImpl(bot) { init { - bot.components.noticeProcessorPipeline.processors.forEach { registerProcessor(it) } + bot.components.noticeProcessorPipeline.processors.forEach { registerProcessor(it.value) } } - override fun createContext(attributes: TypeSafeMap): NoticePipelineContext = + override fun createContext(data: ProtocolStruct, attributes: TypeSafeMap): NoticePipelineContext = createContext(this, attributes) }, block) diff --git a/mirai-core/src/jvmTest/kotlin/message/data/MessageReceiptTest.kt b/mirai-core/src/jvmTest/kotlin/message/data/MessageReceiptTest.kt index 3f67af0f7..9eeba02ba 100644 --- a/mirai-core/src/jvmTest/kotlin/message/data/MessageReceiptTest.kt +++ b/mirai-core/src/jvmTest/kotlin/message/data/MessageReceiptTest.kt @@ -9,35 +9,29 @@ package net.mamoe.mirai.internal.message.data -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import net.mamoe.mirai.Bot -import net.mamoe.mirai.internal.AbstractTestWithMiraiImpl import net.mamoe.mirai.internal.MockBot -import net.mamoe.mirai.internal.contact.* -import net.mamoe.mirai.internal.contact.info.GroupInfoImpl +import net.mamoe.mirai.internal.contact.AbstractContact +import net.mamoe.mirai.internal.message.protocol.MessageProtocolFacade +import net.mamoe.mirai.internal.message.protocol.impl.GeneralMessageSenderProtocol +import net.mamoe.mirai.internal.message.protocol.outgoing.* import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToGroupImpl -import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum -import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.internal.message.source.createMessageReceipt +import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage +import net.mamoe.mirai.internal.notice.processors.GroupExtensions +import net.mamoe.mirai.internal.pipeline.replaceProcessor +import net.mamoe.mirai.internal.test.AbstractTest import net.mamoe.mirai.internal.test.runBlockingUnit -import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.message.data.ForwardMessage +import net.mamoe.mirai.message.data.buildForwardMessage +import net.mamoe.mirai.message.data.toMessageChain import net.mamoe.mirai.utils.currentTimeSeconds import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertSame +import kotlin.test.assertTrue -internal class MessageReceiptTest : AbstractTestWithMiraiImpl() { - override suspend fun uploadMessageHighway( - bot: Bot, - sendMessageHandler: SendMessageHandler<*>, - message: Collection<ForwardMessage.INode>, - isLong: Boolean, - ): String { - return "id" - } - +internal class MessageReceiptTest : AbstractTest(), GroupExtensions { private val bot = MockBot() /** @@ -45,38 +39,56 @@ internal class MessageReceiptTest : AbstractTestWithMiraiImpl() { */ // We need #1304 @Test fun `refine ForwardMessageInternal for MessageReceipt`() = runBlockingUnit { - val group = - GroupImpl(bot, bot.coroutineContext, 1, GroupInfoImpl(StTroopNum(1, 1, dwGroupOwnerUin = 2)), sequenceOf()) + val group = bot.addGroup(123, 2) val forward = buildForwardMessage(group) { 2 says "ok" } val message = forward.toMessageChain() - val handler = object : GroupSendMessageHandler(group) { - override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> = - { _, _, message, _, sourceCallback -> + val facade = MessageProtocolFacade.INSTANCE.copy() - assertIs<ForwardMessageInternal>(message[ForwardMessageInternal]) - assertSame(forward, message[ForwardMessageInternal]?.origin) + assertTrue { + facade.preprocessorPipeline.replaceProcessor( + { it is GeneralMessageSenderProtocol.GeneralMessageSender }, + OutgoingMessageProcessorAdapter(object : OutgoingMessageSender { + override suspend fun OutgoingMessagePipelineContext.process() { + assertIs<ForwardMessageInternal>(currentMessageChain[ForwardMessageInternal]) + assertSame(forward, currentMessageChain[ForwardMessageInternal]?.origin) - sourceCallback( - CompletableDeferred( - OnlineMessageSourceToGroupImpl( - group, - internalIds = intArrayOf(1), - sender = bot, - target = group, - time = currentTimeSeconds().toInt(), - originalMessage = message //, - // sourceMessage = message - ) + val source = OnlineMessageSourceToGroupImpl( + group, + internalIds = intArrayOf(1), + sender = bot, + target = group, + time = currentTimeSeconds().toInt(), + originalMessage = currentMessageChain //, + // sourceMessage = message ) - ) - listOf() - } + + collect(source.createMessageReceipt(group, true)) + } + }) + ) } - val result = handler.sendMessage(message, message, false, SendMessageStep.FIRST) + + val result = facade.preprocessAndSendOutgoing(group, message, ConcurrentComponentStorage { + set(MessageProtocolStrategy, object : GroupMessageProtocolStrategy(group) { + + }) + set(HighwayUploader, object : HighwayUploader { + override suspend fun uploadMessages( + contact: AbstractContact, + strategy: MessageProtocolStrategy<*>, + nodes: Collection<ForwardMessage.INode>, + isLong: Boolean, + facade: MessageProtocolFacade, + senderName: String + ): String { + return "id" + } + }) + }) assertIs<ForwardMessage>(result.source.originalMessage[ForwardMessage]) assertEquals(message, result.source.originalMessage)