Implement long message for private session and unify message send as SendMessageHandler, fix send failure with quoted image fix #892

This commit is contained in:
Him188 2021-01-28 18:04:17 +08:00
parent 32362f02c3
commit 74c4369931
16 changed files with 550 additions and 394 deletions

View File

@ -695,9 +695,9 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
}
internal suspend fun uploadGroupMessageHighway(
internal suspend fun uploadMessageHighway(
bot: Bot,
groupCode: Long,
sendMessageHandler: SendMessageHandler<*>,
message: Collection<ForwardMessage.INode>,
isLong: Boolean,
): String = with(bot.asQQAndroidBot()) {
@ -705,14 +705,12 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
it.messageChain.ensureSequenceIdAvailable()
}
val group = getGroupOrFail(groupCode)
val sequenceId = client.atomicNextMessageSequenceId()
val data = message.calculateValidationDataForGroup(
val data = message.calculateValidationData(
sequenceId = sequenceId,
random = Random.nextInt().absoluteValue,
group
sendMessageHandler
)
val response = network.run {
@ -720,7 +718,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
buType = if (isLong) 1 else 2,
client = bot.client,
messageData = data,
dstUin = Mirai.calculateGroupUinByGroupCode(groupCode)
dstUin = sendMessageHandler.targetUin
).sendAndExpect<MultiMsg.ApplyUp.Response>()
}
@ -740,7 +738,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
msgUpReq = listOf(
LongMsg.MsgUpReq(
msgType = 3, // group
dstUin = Mirai.calculateGroupUinByGroupCode(groupCode),
dstUin = sendMessageHandler.targetUin,
msgId = 0,
msgUkey = response.proto.msgUkey,
needCache = 0,
@ -755,8 +753,8 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
bot = bot,
resource = resource,
kind = when (isLong) {
true -> ResourceKind.GROUP_LONG_MESSAGE
false -> ResourceKind.GROUP_FORWARD_MESSAGE
true -> ResourceKind.LONG_MESSAGE
false -> ResourceKind.FORWARD_MESSAGE
},
commandId = 27,
initialTicket = response.proto.msgSig

View File

@ -17,9 +17,7 @@ import net.mamoe.mirai.contact.Stranger
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.data.UserInfo
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BeforeImageUploadEvent
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.message.OfflineFriendImage
import net.mamoe.mirai.internal.message.getImageType
import net.mamoe.mirai.internal.network.highway.ChannelKind
@ -29,7 +27,11 @@ import net.mamoe.mirai.internal.network.highway.postImage
import net.mamoe.mirai.internal.network.highway.tryServers
import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.utils.*
import kotlin.coroutines.CoroutineContext
@ -135,4 +137,25 @@ internal abstract class AbstractUser(
}
}
}
}
@Suppress("DuplicatedCode")
internal suspend fun <C : User> SendMessageHandler<out C>.sendMessageImpl(
message: Message,
preSendEventConstructor: (C, Message) -> MessagePreSendEvent,
postSendEventConstructor: (C, MessageChain, Throwable?, MessageReceipt<C>?) -> MessagePostSendEvent<C>,
): MessageReceipt<C> {
require(!message.isContentEmpty()) { "message is empty" }
val chain = contact.broadcastMessagePreSendEvent(message, preSendEventConstructor)
val result = this
.runCatching { sendMessage(message, chain, SendMessageStep.FIRST) }
// logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source
contact.logMessageSent(chain)
postSendEventConstructor(contact, chain, result.exceptionOrNull(), result.getOrNull()).broadcast()
return result.getOrThrow()
}

View File

@ -24,12 +24,13 @@ import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.FriendInfoImpl
import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.message.data.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
@ -80,16 +81,12 @@ internal class FriendImpl(
}
}
private val handler: FriendSendMessageHandler by lazy { FriendSendMessageHandler(this) }
@Suppress("DuplicatedCode")
override suspend fun sendMessage(message: Message): MessageReceipt<Friend> {
require(!message.isContentEmpty()) { "message is empty" }
return sendMessageImpl(
message,
friendReceiptConstructor = { MessageReceipt(it, this) },
tReceiptConstructor = { MessageReceipt(it, this) }
).also {
logMessageSent(message)
}
return handler.sendMessageImpl(message, ::FriendMessagePreSendEvent, ::FriendMessagePostSendEvent)
}
override fun toString(): String = "Friend($id)"

View File

@ -120,9 +120,10 @@ internal class GroupImpl(
require(!message.isContentEmpty()) { "message is empty" }
check(!isBotMuted) { throw BotIsBeingMutedException(this) }
val chain = broadcastGroupMessagePreSendEvent(message)
val chain = broadcastMessagePreSendEvent(message, ::GroupMessagePreSendEvent)
val result = sendMessageImpl(message, chain, GroupMessageSendingStep.FIRST)
val result = GroupSendMessageHandler(this)
.runCatching { sendMessage(message, chain, SendMessageStep.FIRST) }
// logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source
logMessageSent(chain)

View File

@ -12,61 +12,22 @@
package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MessageTooLargeException
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.GroupMessagePreSendEvent
import net.mamoe.mirai.event.nextEventOrNull
import net.mamoe.mirai.internal.MiraiImpl
import net.mamoe.mirai.internal.forwardMessage
import net.mamoe.mirai.internal.longMessage
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.SendMessageMultiProtocol
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushGroupMsg
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.currentTimeSeconds
/**
* Might be recalled with [transformedMessage] `is` [LongMessageInternal] if length estimation failed ([sendMessagePacket])
*/
internal suspend fun GroupImpl.sendMessageImpl(
originalMessage: Message,
transformedMessage: Message,
step: GroupMessageSendingStep,
): Result<MessageReceipt<Group>> { // Result<MessageReceipt<Group>>
val chain = transformedMessage
.transformSpecialMessages(this)
.convertToLongMessageIfNeeded(step, this)
chain.findIsInstance<QuoteReply>()?.source?.ensureSequenceIdAvailable()
chain.asSequence().filterIsInstance<FriendImage>().forEach { image ->
updateFriendImageForGroupMessage(image)
}
return kotlin.runCatching {
sendMessagePacket(
originalMessage,
transformedMessage,
chain,
step
)
}
}
import net.mamoe.mirai.event.events.MessagePreSendEvent
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.toMessageChain
/**
* Called only in 'public' apis.
*/
internal suspend fun GroupImpl.broadcastGroupMessagePreSendEvent(message: Message): MessageChain {
internal suspend fun <C : Contact> C.broadcastMessagePreSendEvent(
message: Message,
eventConstructor: (C, Message) -> MessagePreSendEvent
): MessageChain {
return kotlin.runCatching {
GroupMessagePreSendEvent(this, message).broadcast()
eventConstructor(this, message).broadcast()
}.onSuccess {
check(!it.isCancelled) {
throw EventCancelledException("cancelled by GroupMessagePreSendEvent")
@ -77,178 +38,6 @@ internal suspend fun GroupImpl.broadcastGroupMessagePreSendEvent(message: Messag
}
/**
* - [ForwardMessage] -> [ForwardMessageInternal] (by uploading through highway)
* - ... any others for future
*/
private suspend fun Message.transformSpecialMessages(contact: Contact): MessageChain {
return takeSingleContent<ForwardMessage>()?.let { forward ->
check(forward.nodeList.size <= 200) {
throw MessageTooLargeException(
contact, forward, forward,
"ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
)
}
val resId = MiraiImpl.uploadGroupMessageHighway(contact.bot, contact.id, forward.nodeList, false)
RichMessage.forwardMessage(
resId = resId,
timeSeconds = currentTimeSeconds(),
forwardMessage = forward,
)
}?.toMessageChain() ?: toMessageChain()
}
internal enum class GroupMessageSendingStep {
internal enum class SendMessageStep {
FIRST, LONG_MESSAGE, FRAGMENTED
}
/**
* Final process
*/
private suspend fun GroupImpl.sendMessagePacket(
originalMessage: Message,
transformedMessage: Message,
finalMessage: MessageChain,
step: GroupMessageSendingStep,
): MessageReceipt<Group> {
val group = this
var source: OnlineMessageSourceToGroupImpl? = null
bot.network.run {
SendMessageMultiProtocol.createToGroup(
bot.client, group, finalMessage,
step == GroupMessageSendingStep.FRAGMENTED
) { source = it }.forEach { packet ->
when (val resp = packet.sendAndExpect<Packet>()) {
is MessageSvcPbSendMsg.Response -> {
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return when (step) {
GroupMessageSendingStep.FIRST -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.LONG_MESSAGE
)
}
GroupMessageSendingStep.LONG_MESSAGE -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.FRAGMENTED
)
}
else -> {
throw MessageTooLargeException(
group,
originalMessage,
finalMessage,
"Message '${finalMessage.content.take(10)}' is too large."
)
}
}.getOrThrow()
}
check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send group message failed: $resp"
}
}
is MusicSharePacket.Response -> {
resp.pkg.checkSuccess("send group music share")
val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt =
nextEventOrNull(3000) { it.fromAppId == 3116 }
?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY
source = OnlineMessageSourceToGroupImpl(
group,
internalIds = intArrayOf(receipt.messageRandom),
providedSequenceIds = intArrayOf(receipt.sequenceId),
sender = bot,
target = group,
time = currentTimeSeconds().toInt(),
originalMessage = finalMessage
)
}
}
}
check(source != null) {
"Internal error: source is not initialized"
}
try {
source!!.ensureSequenceIdAvailable()
} catch (e: Exception) {
bot.network.logger.warning(
"Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
e
)
}
return MessageReceipt(source!!, group)
}
}
private suspend fun GroupImpl.uploadGroupLongMessageHighway(
chain: MessageChain
) = MiraiImpl.uploadGroupMessageHighway(
bot, this.id,
listOf(
ForwardMessage.Node(
senderId = bot.id,
time = currentTimeSeconds().toInt(),
messageChain = chain,
senderName = bot.nick
)
),
true
)
private suspend fun MessageChain.convertToLongMessageIfNeeded(
step: GroupMessageSendingStep,
groupImpl: GroupImpl,
): MessageChain {
suspend fun sendLongImpl(): MessageChain {
val resId = groupImpl.uploadGroupLongMessageHighway(this)
return this + RichMessage.longMessage(
brief = takeContent(27),
resId = resId,
timeSeconds = currentTimeSeconds()
) // LongMessageInternal replaces all contents and preserves metadata
}
return when (step) {
GroupMessageSendingStep.FIRST -> {
// 只需要在第一次发送的时候验证长度
// 后续重试直接跳过
if (contains(ForceAsLongMessage)) {
sendLongImpl()
}
verityLength(this, groupImpl)
this
}
GroupMessageSendingStep.LONG_MESSAGE -> {
sendLongImpl()
}
GroupMessageSendingStep.FRAGMENTED -> this
}
}
/**
* Ensures server holds the cache
*/
private suspend fun GroupImpl.updateFriendImageForGroupMessage(image: FriendImage) {
bot.network.run {
ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = id,
md5 = image.md5,
size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0
).sendAndExpect<ImgStore.GroupPicUp.Response>()
}
}

View File

@ -20,13 +20,10 @@ import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.message.OnlineMessageSourceToTempImpl
import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
import net.mamoe.mirai.internal.message.firstIsInstanceOrNull
import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToTemp
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.currentTimeSeconds
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -48,66 +45,24 @@ internal class NormalMemberImpl constructor(
override fun toString(): String = "NormalMember($id)"
@Suppress("UNCHECKED_CAST")
@JvmSynthetic
private val handler by lazy { GroupTempSendMessageHandler(this) }
@Suppress("DuplicatedCode")
override suspend fun sendMessage(message: Message): MessageReceipt<NormalMember> {
require(!message.isContentEmpty()) { "message is empty" }
val asFriend = this.asFriendOrNull()
val asStranger = this.asStrangerOrNull()
return (asFriend?.sendMessageImpl(
message,
friendReceiptConstructor = { MessageReceipt(it, asFriend) },
tReceiptConstructor = { MessageReceipt(it, this) }
) ?: asStranger?.sendMessageImpl(
message,
strangerReceiptConstructor = { MessageReceipt(it, asStranger) },
tReceiptConstructor = { MessageReceipt(it, this) }
) ?: sendMessageImpl(message)).also { logMessageSent(message) }
return asFriendOrNull()?.sendMessage(message)?.convert()
?: asStrangerOrNull()?.sendMessage(message)?.convert()
?: handler.sendMessageImpl<NormalMember>(
message = message,
preSendEventConstructor = ::GroupTempMessagePreSendEvent,
postSendEventConstructor = ::GroupTempMessagePostSendEvent.cast()
)
}
private suspend fun sendMessageImpl(message: Message): MessageReceipt<NormalMember> {
val chain = kotlin.runCatching {
GroupTempMessagePreSendEvent(this, message).broadcast()
}.onSuccess {
check(!it.isCancelled) {
throw EventCancelledException("cancelled by GroupTempMessagePreSendEvent")
}
}.getOrElse {
throw EventCancelledException("exception thrown when broadcasting GroupTempMessagePreSendEvent", it)
}.message.toMessageChain()
chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
val result = bot.network.runCatching {
val source: OnlineMessageSourceToTempImpl
MessageSvcPbSendMsg.createToTemp(
bot.client,
this@NormalMemberImpl,
chain
) {
source = it
}.sendAndExpect<MessageSvcPbSendMsg.Response>().let {
check(it is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send temp message failed: $it"
}
}
MessageReceipt(source, this@NormalMemberImpl)
}
result.fold(
onSuccess = {
GroupTempMessagePostSendEvent(this, chain, null, it)
},
onFailure = {
GroupTempMessagePostSendEvent(this, chain, it, null)
}
).broadcast()
return result.getOrThrow()
private fun MessageReceipt<User>.convert(): MessageReceipt<NormalMemberImpl> {
return MessageReceipt(OnlineMessageSourceToTempImpl(source, this@NormalMemberImpl), this@NormalMemberImpl)
}
@Suppress("PropertyName")
internal var _nameCard: String = memberInfo.nameCard

View File

@ -0,0 +1,357 @@
/*
* 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 net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.nextEventOrNull
import net.mamoe.mirai.internal.MiraiImpl
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.forwardMessage
import net.mamoe.mirai.internal.longMessage
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToFriend
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.currentTimeSeconds
import java.lang.UnsupportedOperationException
/**
* 通用处理器
*/
internal abstract class SendMessageHandler<C : Contact> {
abstract val contact: C
abstract val senderName: String
val messageSourceKind: MessageSourceKind
get() {
return when (contact) {
is Group -> MessageSourceKind.GROUP
is Friend -> MessageSourceKind.FRIEND
is Member -> MessageSourceKind.TEMP
is Stranger -> MessageSourceKind.STRANGER
else -> error("Unsupported contact: $contact")
}
}
val bot get() = contact.bot.asQQAndroidBot()
val targetUserUin: Long? get() = contact.castOrNull<User>()?.uin
val targetGroupUin: Long? get() = contact.castOrNull<Group>()?.uin
val targetGroupCode: Long? get() = contact.castOrNull<Group>()?.groupCode
val targetOtherClientBotUin: Long? get() = contact.castOrNull<OtherClient>()?.bot?.id
val targetUin: Long get() = targetGroupUin ?: targetOtherClientBotUin ?: contact.id
val groupInfo: MsgComm.GroupInfo?
get() = if (isToGroup) MsgComm.GroupInfo(
groupCode = targetGroupCode!!,
groupCard = senderName // Cinnamon
) else null
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)) {
sendLongImpl()
}
if (!contains(IgnoreLengthCheck)) {
verityLength(this, contact)
}
this
}
SendMessageStep.LONG_MESSAGE -> {
if (contains(DontAsLongMessage)) this // fragmented
else sendLongImpl()
}
SendMessageStep.FRAGMENTED -> this
}
}
/**
* Final process
*/
suspend fun sendMessagePacket(
originalMessage: Message,
transformedMessage: Message,
finalMessage: MessageChain,
step: SendMessageStep,
): MessageReceipt<C> {
val group = contact
var source: OnlineMessageSource.Outgoing? = null
bot.network.run {
sendMessageMultiProtocol(
bot.client, finalMessage,
fragmented = step == SendMessageStep.FRAGMENTED
) { source = it }.forEach { packet ->
when (val resp = packet.sendAndExpect<Packet>()) {
is MessageSvcPbSendMsg.Response -> {
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return when (step) {
SendMessageStep.FIRST -> {
sendMessage(originalMessage, transformedMessage, SendMessageStep.LONG_MESSAGE)
}
SendMessageStep.LONG_MESSAGE -> {
sendMessage(originalMessage, transformedMessage, SendMessageStep.FRAGMENTED)
}
else -> {
throw MessageTooLargeException(
group,
originalMessage,
finalMessage,
"Message '${finalMessage.content.take(10)}' is too large."
)
}
}
}
check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send group message failed: $resp"
}
}
is MusicSharePacket.Response -> {
resp.pkg.checkSuccess("send group music share")
source = constructSourceFromMusicShareResponse(finalMessage, resp)
}
}
}
check(source != null) {
"Internal error: source is not initialized"
}
try {
source!!.ensureSequenceIdAvailable()
} catch (e: Exception) {
bot.network.logger.warning(
"Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
e
)
}
return MessageReceipt(source!!, contact)
}
}
fun sendMessageMultiProtocol(
client: QQAndroidClient,
message: MessageChain,
fragmented: Boolean,
sourceCallback: (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
)
)
}
return messageSvcSendMessage(client, contact, message, fragmented, sourceCallback)
}
abstract val messageSvcSendMessage: (
client: QQAndroidClient,
contact: C,
message: MessageChain,
fragmented: Boolean,
sourceCallback: (OnlineMessageSource.Outgoing) -> Unit,
) -> List<OutgoingPacket>
abstract suspend fun constructSourceFromMusicShareResponse(
finalMessage: MessageChain,
response: MusicSharePacket.Response
): OnlineMessageSource.Outgoing
open suspend fun uploadLongMessageHighway(
chain: MessageChain
): String = with(contact) {
return MiraiImpl.uploadMessageHighway(
bot, this@SendMessageHandler,
listOf(
ForwardMessage.Node(
senderId = bot.id,
time = currentTimeSeconds().toInt(),
messageChain = chain,
senderName = bot.nick
)
),
true
)
}
open suspend fun postTransformActions(chain: MessageChain) {
}
}
/**
* - [ForwardMessage] -> [ForwardMessageInternal] (by uploading through highway)
* - ... any others for future
*/
internal suspend fun <C : Contact> SendMessageHandler<C>.transformSpecialMessages(message: Message): MessageChain {
return message.takeSingleContent<ForwardMessage>()?.let { forward ->
check(forward.nodeList.size <= 200) {
throw MessageTooLargeException(
contact, forward, forward,
"ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
)
}
val resId = MiraiImpl.uploadMessageHighway(
bot = contact.bot,
sendMessageHandler = this,
message = forward.nodeList,
isLong = false,
)
RichMessage.forwardMessage(
resId = resId,
timeSeconds = currentTimeSeconds(),
forwardMessage = forward,
)
}?.toMessageChain() ?: message.toMessageChain()
}
/**
* Might be recalled with [transformedMessage] `is` [LongMessageInternal] if length estimation failed ([sendMessagePacket])
*/
internal suspend fun <C : Contact> SendMessageHandler<C>.sendMessage(
originalMessage: Message,
transformedMessage: Message,
step: SendMessageStep,
): MessageReceipt<C> { // Result cannot be in interface.
val chain = transformSpecialMessages(transformedMessage)
.convertToLongMessageIfNeeded(step)
chain.findIsInstance<QuoteReply>()?.source?.ensureSequenceIdAvailable()
postTransformActions(chain)
return sendMessagePacket(originalMessage, transformedMessage, chain, step)
}
internal sealed class UserSendMessageHandler<C : AbstractUser>(
override val contact: C,
) : SendMessageHandler<C>() {
override val senderName: String get() = bot.nick
override suspend fun constructSourceFromMusicShareResponse(
finalMessage: MessageChain,
response: MusicSharePacket.Response
): OnlineMessageSource.Outgoing {
throw UnsupportedOperationException("Sending MusicShare 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: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToFriend
}
internal class StrangerSendMessageHandler(
contact: StrangerImpl,
) : UserSendMessageHandler<StrangerImpl>(contact) {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (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: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToTemp
}
internal class GroupSendMessageHandler(
override val contact: GroupImpl,
) : SendMessageHandler<GroupImpl>() {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToGroup
override val senderName: String
get() = contact.botAsMember.nameCardOrNick
override suspend fun postTransformActions(chain: MessageChain) {
chain.asSequence().filterIsInstance<FriendImage>().forEach { image ->
contact.updateFriendImageForGroupMessage(image)
}
}
override suspend fun constructSourceFromMusicShareResponse(
finalMessage: MessageChain,
response: MusicSharePacket.Response
): OnlineMessageSource.Outgoing {
val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt =
nextEventOrNull(3000) { it.fromAppId == 3116 }
?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY
return OnlineMessageSourceToGroupImpl(
contact,
internalIds = intArrayOf(receipt.messageRandom),
providedSequenceIds = intArrayOf(receipt.sequenceId),
sender = bot,
target = contact,
time = currentTimeSeconds().toInt(),
originalMessage = finalMessage
)
}
companion object {
/**
* Ensures server holds the cache
*/
private suspend fun GroupImpl.updateFriendImageForGroupMessage(image: FriendImage) {
bot.network.run {
ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = id,
md5 = image.md5,
size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0
).sendAndExpect<ImgStore.GroupPicUp.Response>()
}
}
}
}

View File

@ -20,14 +20,17 @@ package net.mamoe.mirai.internal.contact
import kotlinx.atomicfu.AtomicInt
import kotlinx.atomicfu.atomic
import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.contact.Stranger
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.FriendInfoImpl
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.OnlineMessageSourceToStrangerImpl
import net.mamoe.mirai.internal.network.protocol.packet.list.StrangerList
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.utils.cast
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
@ -75,16 +78,20 @@ internal class StrangerImpl(
}
}
private val handler by lazy { StrangerSendMessageHandler(this) }
@Suppress("DuplicatedCode")
override suspend fun sendMessage(message: Message): MessageReceipt<Stranger> {
require(!message.isContentEmpty()) { "message is empty" }
return sendMessageImpl(
message,
strangerReceiptConstructor = { MessageReceipt(it, this) },
tReceiptConstructor = { MessageReceipt(it, this) }
).also {
logMessageSent(message)
}
return asFriendOrNull()?.sendMessage(message)?.convert()
?: handler.sendMessageImpl<Stranger>(
message = message,
preSendEventConstructor = ::StrangerMessagePreSendEvent,
postSendEventConstructor = ::StrangerMessagePostSendEvent.cast()
)
}
private fun MessageReceipt<User>.convert(): MessageReceipt<StrangerImpl> {
return MessageReceipt(OnlineMessageSourceToStrangerImpl(source, this@StrangerImpl), this@StrangerImpl)
}
override fun toString(): String = "Stranger($id)"

View File

@ -58,12 +58,14 @@ internal suspend fun <T : User> Friend.sendMessageImpl(
chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
lateinit var source: OnlineMessageSourceToFriendImpl
val result = bot.network.runCatching {
MessageSvcPbSendMsg.createToFriend(
bot.client,
this@sendMessageImpl,
chain
chain,
false
) {
source = it
}.forEach { packet ->
@ -116,12 +118,16 @@ internal suspend fun <T : User> Stranger.sendMessageImpl(
bot.client,
this@sendMessageImpl,
chain,
false,
) {
source = it
}.sendAndExpect<MessageSvcPbSendMsg.Response>().let {
check(it is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send stranger message failed: $it"
}
}.forEach { pk ->
pk.sendAndExpect<net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg.Response>()
.let {
kotlin.check(it is net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg.Response.SUCCESS) {
"Send temp message failed: $it"
}
}
}
strangerReceiptConstructor(source)
}

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.internal.message
import net.mamoe.mirai.message.data.*
@ -19,7 +21,24 @@ internal object ForceAsLongMessage : MessageMetadata, ConstrainSingle, InternalF
AbstractMessageKey<ForceAsLongMessage>({ it.safeCast() }) {
override val key: MessageKey<ForceAsLongMessage> get() = this
override fun toString(): String = "ForceLongMessage"
override fun toString(): String = ""
}
/**
* 强制不发 long
*/
internal object DontAsLongMessage : MessageMetadata, ConstrainSingle, InternalFlagOnlyMessage,
AbstractMessageKey<DontAsLongMessage>({ it.safeCast() }) {
override val key: MessageKey<DontAsLongMessage> get() = this
override fun toString(): String = ""
}
internal object IgnoreLengthCheck : MessageMetadata, ConstrainSingle, InternalFlagOnlyMessage,
AbstractMessageKey<IgnoreLengthCheck>({ it.safeCast() }) {
override val key: MessageKey<IgnoreLengthCheck> get() = this
override fun toString(): String = ""
}
/**

View File

@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean
internal interface MessageSourceInternal {
@Transient
val sequenceIds: IntArray
val sequenceIds: IntArray // ids
@Transient
val internalIds: IntArray // randomId

View File

@ -103,6 +103,12 @@ internal class OnlineMessageSourceToStrangerImpl(
override val sender: Bot,
override val target: Stranger
) : OnlineMessageSource.Outgoing.ToStranger(), MessageSourceInternal {
constructor(
delegate: OnlineMessageSource.Outgoing,
target: Stranger
) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target)
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToStranger")
override val bot: Bot
@ -123,6 +129,11 @@ internal class OnlineMessageSourceToTempImpl(
override val sender: Bot,
override val target: Member
) : OnlineMessageSource.Outgoing.ToTemp(), MessageSourceInternal {
constructor(
delegate: OnlineMessageSource.Outgoing,
target: Member
) : this(delegate.ids, delegate.internalIds, delegate.time, delegate.originalMessage, delegate.sender, target)
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToTemp")
override val bot: Bot

View File

@ -89,8 +89,8 @@ internal enum class ResourceKind(
PRIVATE_VOICE("private voice"),
GROUP_VOICE("group voice"),
GROUP_LONG_MESSAGE("group long message"),
GROUP_FORWARD_MESSAGE("group forward message"),
LONG_MESSAGE("long message"),
FORWARD_MESSAGE("forward message"),
;
override fun toString(): String = display

View File

@ -12,9 +12,8 @@
package net.mamoe.mirai.internal.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.groupCode
import net.mamoe.mirai.internal.contact.SendMessageHandler
import net.mamoe.mirai.internal.message.toRichTextElems
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
@ -45,15 +44,16 @@ internal class MessageValidationData(
}
}
internal fun Collection<ForwardMessage.INode>.calculateValidationDataForGroup(
internal fun Collection<ForwardMessage.INode>.calculateValidationData(
sequenceId: Int,
random: Int,
targetGroup: Group,
handler: SendMessageHandler<*>,
): MessageValidationData {
val msgList = map { chain ->
MsgComm.Msg(
msgHead = MsgComm.MsgHead(
fromUin = chain.senderId,
toUin = handler.targetUserUin ?: 0,
msgSeq = sequenceId,
msgTime = chain.time,
msgUid = 0x01000000000000000L or random.toLongUnsigned(),
@ -62,16 +62,13 @@ internal fun Collection<ForwardMessage.INode>.calculateValidationDataForGroup(
msgId = 1
),
msgType = 82, // troop
groupInfo = MsgComm.GroupInfo(
groupCode = targetGroup.groupCode,
groupCard = chain.senderName // Cinnamon
),
groupInfo = handler.groupInfo,
isSrcMsg = false
),
msgBody = ImMsgBody.MsgBody(
richText = ImMsgBody.RichText(
elems = chain.messageChain.toMessageChain()
.toRichTextElems(targetGroup, withGeneralFlags = false).toMutableList()
.toRichTextElems(handler.contact, withGeneralFlags = false).toMutableList()
)
)
)

View File

@ -9,33 +9,3 @@
package net.mamoe.mirai.internal.network.protocol.packet.chat
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.internal.contact.takeSingleContent
import net.mamoe.mirai.internal.message.OnlineMessageSourceToGroupImpl
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSourceKind
import net.mamoe.mirai.message.data.MusicShare
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
internal object SendMessageMultiProtocol {
inline fun createToGroup(
client: QQAndroidClient,
group: Group,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit
): List<OutgoingPacket> {
contract { callsInPlace(sourceCallback, InvocationKind.AT_MOST_ONCE) }
message.takeSingleContent<MusicShare>()?.let { musicShare ->
return listOf(MusicSharePacket(client, musicShare, group.id, targetKind = MessageSourceKind.GROUP))
}
return MessageSvcPbSendMsg.createToGroup(client, group, message, fragmented, sourceCallback)
}
}

View File

@ -146,29 +146,55 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
* 发送陌生人消息
*/
@Suppress("FunctionName")
internal fun createToStrangerImpl(
internal inline fun createToStrangerImpl(
client: QQAndroidClient,
target: Stranger,
message: MessageChain,
source: OnlineMessageSourceToStrangerImpl
): OutgoingPacket = buildOutgoingUniPacket(client) {
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
fragmented: Boolean,
source: (OnlineMessageSourceToStrangerImpl) -> Unit
): List<OutgoingPacket> {
///return@buildOutgoingUniPacket
writeProtoBuf(
MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
routingHead = MsgSvc.RoutingHead(c2c = MsgSvc.C2C(toUin = target.uin)),
contentHead = MsgComm.ContentHead(pkgNum = 1),
msgBody = ImMsgBody.MsgBody(
val sequenceIds = AtomicReference<IntArray>()
val randIds = AtomicReference<IntArray>()
return buildOutgoingMessageCommon(
client = client,
message = message,
fragmentTranslator = {
ImMsgBody.MsgBody(
richText = ImMsgBody.RichText(
elems = message.toRichTextElems(messageTarget = target, withGeneralFlags = true)
elems = it.toRichTextElems(messageTarget = target, withGeneralFlags = true)
)
),
msgSeq = source.sequenceIds.single(),
msgRand = source.internalIds.single(),
syncCookie = client.syncingController.syncCookie ?: byteArrayOf()
// msgVia = 1
)
)
},
pbSendMsgReq = { msgBody, msgSeq, msgRand, contentHead ->
MsgSvc.PbSendMsgReq(
routingHead = MsgSvc.RoutingHead(c2c = MsgSvc.C2C(toUin = target.uin)),
contentHead = contentHead,
msgBody = msgBody,
msgSeq = msgSeq,
msgRand = msgRand,
syncCookie = client.syncingController.syncCookie ?: byteArrayOf()
// msgVia = 1
)
},
sequenceIds = sequenceIds,
randIds = randIds,
sequenceIdsInitializer = { size ->
IntArray(size) { client.nextFriendSeq() }
},
postInit = {
source(
OnlineMessageSourceToStrangerImpl(
internalIds = randIds.get(),
sender = client.bot,
target = target,
time = currentTimeSeconds().toInt(),
sequenceIds = sequenceIds.get(),
originalMessage = message
)
)
},
doFragmented = fragmented
)
}
@ -180,6 +206,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
client: QQAndroidClient,
targetFriend: Friend,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToFriendImpl) -> Unit
): List<OutgoingPacket> {
contract {
@ -225,7 +252,8 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
originalMessage = message
)
)
}
},
doFragmented = fragmented
)
}
/*= buildOutgoingUniPacket(client) {
@ -269,6 +297,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
client: QQAndroidClient,
targetMember: Member,
message: MessageChain,
fragmented: Boolean,
source: OnlineMessageSourceToTempImpl
): OutgoingPacket = buildOutgoingUniPacket(client) {
writeProtoBuf(
@ -439,8 +468,9 @@ internal inline fun MessageSvcPbSendMsg.createToTemp(
client: QQAndroidClient,
member: Member,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToTempImpl) -> Unit
): OutgoingPacket {
): List<OutgoingPacket> {
contract {
callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE)
}
@ -457,33 +487,27 @@ internal inline fun MessageSvcPbSendMsg.createToTemp(
client,
member,
message,
fragmented,
source
)
).let { listOf(it) }
}
internal inline fun MessageSvcPbSendMsg.createToStranger(
client: QQAndroidClient,
stranger: Stranger,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToStrangerImpl) -> Unit
): OutgoingPacket {
): List<OutgoingPacket> {
contract {
callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE)
}
val source = OnlineMessageSourceToStrangerImpl(
internalIds = intArrayOf(Random.nextInt().absoluteValue),
sender = client.bot,
target = stranger,
time = currentTimeSeconds().toInt(),
sequenceIds = intArrayOf(client.atomicNextMessageSequenceId()),
originalMessage = message
)
sourceCallback(source)
return createToStrangerImpl(
client,
stranger,
message,
source
fragmented,
sourceCallback
)
}
@ -491,6 +515,7 @@ internal inline fun MessageSvcPbSendMsg.createToFriend(
client: QQAndroidClient,
qq: Friend,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToFriendImpl) -> Unit
): List<OutgoingPacket> {
contract {
@ -500,6 +525,7 @@ internal inline fun MessageSvcPbSendMsg.createToFriend(
client,
qq,
message,
fragmented,
sourceCallback
)
}