Rewrite Group sendMessageImpl, improve current Highway impls, for music share #889 #682 and further friend message highway #194, #577

This commit is contained in:
Him188 2021-01-23 00:09:35 +08:00
parent 66ae897f20
commit d60006376c
12 changed files with 360 additions and 294 deletions

View File

@ -265,17 +265,4 @@ public class XmlMessageBuilder(
this.builder.append("<picture cover='$coverUrl'/>") this.builder.append("<picture cover='$coverUrl'/>")
} }
} }
}
// internal runtime value, not serializable
internal data class LongMessage internal constructor(override val content: String, val resId: String) :
AbstractServiceMessage() {
override val serviceId: Int get() = 35
companion object Key : AbstractPolymorphicMessageKey<ServiceMessage, LongMessage>(ServiceMessage, { it.safeCast() })
}
// internal runtime value, not serializable
internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() {
override val serviceId: Int get() = 35
} }

View File

@ -24,7 +24,7 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.contact.*
import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.highway.HighwayHelper import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo
import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.* import net.mamoe.mirai.internal.network.protocol.packet.chat.*
@ -32,7 +32,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.MessageSerializers
import net.mamoe.mirai.message.action.Nudge import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
@ -690,102 +689,76 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) } return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
} }
@JvmSynthetic internal suspend fun uploadGroupMessageHighway(
@LowLevelApi
@MiraiExperimentalApi
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal suspend fun lowLevelSendGroupLongOrForwardMessage(
bot: Bot, bot: Bot,
groupCode: Long, groupCode: Long,
message: Collection<ForwardMessage.INode>, message: Collection<ForwardMessage.INode>,
isLong: Boolean, isLong: Boolean,
forwardMessage: ForwardMessage? ): String = with(bot.asQQAndroidBot()) {
): MessageReceipt<Group> = with(bot.asQQAndroidBot()) {
message.forEach { message.forEach {
it.messageChain.ensureSequenceIdAvailable() it.messageChain.ensureSequenceIdAvailable()
} }
val group = getGroupOrFail(groupCode) val group = getGroupOrFail(groupCode)
val time = currentTimeSeconds()
val sequenceId = client.atomicNextMessageSequenceId() val sequenceId = client.atomicNextMessageSequenceId()
network.run { val data = message.calculateValidationDataForGroup(
val data = message.calculateValidationDataForGroup( sequenceId = sequenceId,
sequenceId = sequenceId, random = Random.nextInt().absoluteValue,
random = Random.nextInt().absoluteValue, group
group )
)
val response = val response = network.run {
MultiMsg.ApplyUp.createForGroupLongMessage( MultiMsg.ApplyUp.createForGroup(
buType = if (isLong) 1 else 2, buType = if (isLong) 1 else 2,
client = bot.client, client = bot.client,
messageData = data, messageData = data,
dstUin = Mirai.calculateGroupUinByGroupCode(groupCode) dstUin = Mirai.calculateGroupUinByGroupCode(groupCode)
).sendAndExpect<MultiMsg.ApplyUp.Response>() ).sendAndExpect<MultiMsg.ApplyUp.Response>()
}
val resId: String val resId: String
when (response) { when (response) {
is MultiMsg.ApplyUp.Response.MessageTooLarge -> is MultiMsg.ApplyUp.Response.MessageTooLarge ->
error( error(
"Internal error: message is too large, but this should be handled before sending. " "Internal error: message is too large, but this should be handled before sending. "
)
is MultiMsg.ApplyUp.Response.RequireUpload -> {
resId = response.proto.msgResid
val body = LongMsg.ReqBody(
subcmd = 1,
platformType = 9,
termType = 5,
msgUpReq = listOf(
LongMsg.MsgUpReq(
msgType = 3, // group
dstUin = Mirai.calculateGroupUinByGroupCode(groupCode),
msgId = 0,
msgUkey = response.proto.msgUkey,
needCache = 0,
storeType = 2,
msgContent = data.data
)
)
).toByteArray(LongMsg.ReqBody.serializer())
HighwayHelper.uploadImageToServers(
bot,
response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
response.proto.msgSig,
body.toExternalResource(null),
"group long message",
27
)
}
}
if (isLong) {
group.sendMessage(
RichMessage.longMessage(
brief = message.joinToString(limit = 27) { it.messageChain.contentToString() },
resId = resId,
timeSeconds = time
)
) )
} else { is MultiMsg.ApplyUp.Response.RequireUpload -> {
checkNotNull(forwardMessage) { "Internal error: forwardMessage is null when sending forward" } resId = response.proto.msgResid
group.sendMessage(
RichMessage.forwardMessage( val body = LongMsg.ReqBody(
resId = resId, subcmd = 1,
timeSeconds = time, platformType = 9,
// preview = message.take(5).joinToString { termType = 5,
// """ msgUpReq = listOf(
// <title size="26" color="#777777" maxLines="2" lineSpace="12">${it.message.asMessageChain().joinToString(limit = 10)}</title> LongMsg.MsgUpReq(
// """.trimIndent() msgType = 3, // group
// }, dstUin = Mirai.calculateGroupUinByGroupCode(groupCode),
forwardMessage = forwardMessage, msgId = 0,
msgUkey = response.proto.msgUkey,
needCache = 0,
storeType = 2,
msgContent = data.data
)
) )
).toByteArray(LongMsg.ReqBody.serializer())
Highway.uploadResource(
bot,
response.proto.uint32UpIp.zip(response.proto.uint32UpPort),
response.proto.msgSig,
body.toExternalResource(null),
when (isLong) {
true -> "group long message"
false -> "group forward message"
},
27
) )
} }
} }
return resId
} }

View File

@ -131,7 +131,7 @@ internal class QQAndroidBot constructor(
internal val EMPTY_BYTE_ARRAY = ByteArray(0) internal val EMPTY_BYTE_ARRAY = ByteArray(0)
internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage { internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSeconds: Long): LongMessageInternal {
val limited: String = if (brief.length > 30) { val limited: String = if (brief.length > 30) {
brief.take(30) + "" brief.take(30) + ""
} else { } else {
@ -154,7 +154,7 @@ internal fun RichMessage.Key.longMessage(brief: String, resId: String, timeSecon
</msg> </msg>
""".trimIndent() """.trimIndent()
return LongMessage(template, resId) return LongMessageInternal(template, resId)
} }

View File

@ -18,15 +18,11 @@ import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.MiraiImpl
import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.message.firstIsInstanceOrNull
import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.internal.network.highway.HighwayHelper import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore 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.createToGroup
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
@ -118,142 +114,13 @@ internal class GroupImpl(
require(!message.isContentEmpty()) { "message is empty" } require(!message.isContentEmpty()) { "message is empty" }
check(!isBotMuted) { throw BotIsBeingMutedException(this) } check(!isBotMuted) { throw BotIsBeingMutedException(this) }
return sendMessageImpl(message, false).also { val chain = broadcastGroupMessagePreSendEvent(message)
logMessageSent(message)
return sendMessageImpl(message, chain, false).also {
logMessageSent(chain)
} }
} }
private suspend fun sendMessageImpl(message: Message, isForward: Boolean): MessageReceipt<Group> {
if (message is MessageChain) {
if (message.anyIsInstance<ForwardMessage>()) {
return sendMessageImpl(message.singleOrNull() ?: error("ForwardMessage must be standalone"), true)
}
}
val forward = message as? ForwardMessage ?: message.castOrNull<MessageChain>()?.findIsInstance<ForwardMessage>()
if (forward != null) {
check(forward.nodeList.size < 200) {
throw MessageTooLargeException(
this, forward, forward,
"ForwardMessage allows up to 200 nodes, but found ${forward.nodeList.size}"
)
}
return MiraiImpl.lowLevelSendGroupLongOrForwardMessage(bot, this.id, forward.nodeList, false, forward)
}
val isLongOrForward = message is LongMessage || message is ForwardMessageInternal
val msg: MessageChain = if (!isLongOrForward) {
val chain = kotlin.runCatching {
GroupMessagePreSendEvent(this, message).broadcast()
}.onSuccess {
check(!it.isCancelled) {
throw EventCancelledException("cancelled by GroupMessagePreSendEvent")
}
}.getOrElse {
throw EventCancelledException("exception thrown when broadcasting GroupMessagePreSendEvent", it)
}.message.toMessageChain()
@Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER")
var length = 0
@Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER") // stupid compiler
var imageCnt = 0
chain.verityLength(message, this, lengthCallback = {
length = it
}, imageCntCallback = {
imageCnt = it
})
if (length > 702 || imageCnt > 1) { // 阈值为700左右限制到3的倍数
return MiraiImpl.lowLevelSendGroupLongOrForwardMessage(
bot,
this.id,
listOf(
ForwardMessage.Node(
senderId = bot.id,
time = currentTimeSeconds().toInt(),
messageChain = chain,
senderName = bot.nick
)
),
true, null
)
}
chain
} else message.toMessageChain()
msg.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
msg.filterIsInstance<FriendImage>().forEach { image ->
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>()
}
}
val result = bot.network.runCatching sendMsg@{
val source: OnlineMessageSourceToGroupImpl
MessageSvcPbSendMsg.createToGroup(
bot.client,
this@GroupImpl,
msg,
isForward
) {
source = it
}.sendAndExpect<MessageSvcPbSendMsg.Response>().let {
if (!isLongOrForward && it is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return@sendMsg MiraiImpl.lowLevelSendGroupLongOrForwardMessage(
bot,
this@GroupImpl.id,
listOf(
ForwardMessage.Node(
senderId = bot.id,
time = currentTimeSeconds().toInt(),
messageChain = msg,
senderName = bot.nick
)
),
true, null
)
}
check(it is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send group message failed: $it"
}
}
try {
source.ensureSequenceIdAvailable()
} catch (e: Exception) {
bot.network.logger.warning {
"Timeout awaiting sequenceId for group message(${
message.contentToString()
.take(10)
}). Some features may not work properly"
}
bot.network.logger.warning(e)
}
MessageReceipt(source, this@GroupImpl)
}
result.fold(
onSuccess = {
GroupMessagePostSendEvent(this, msg, null, it)
},
onFailure = {
GroupMessagePostSendEvent(this, msg, it, null)
}
).broadcast()
return result.getOrThrow()
}
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
override suspend fun uploadImage(resource: ExternalResource): Image { override suspend fun uploadImage(resource: ExternalResource): Image {
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) { if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
@ -280,7 +147,7 @@ internal class GroupImpl(
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() } .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
} }
is ImgStore.GroupPicUp.Response.RequireUpload -> { is ImgStore.GroupPicUp.Response.RequireUpload -> {
HighwayHelper.uploadImageToServers( Highway.uploadResource(
bot, bot,
response.uploadIpList.zip(response.uploadPortList), response.uploadIpList.zip(response.uploadPortList),
response.uKey, response.uKey,
@ -304,7 +171,7 @@ internal class GroupImpl(
val response: PttStore.GroupPttUp.Response.RequireUpload = val response: PttStore.GroupPttUp.Response.RequireUpload =
PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect() PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()
HighwayHelper.uploadPttToServers( Highway.uploadPttToServers(
bot, bot,
response.uploadIpList.zip(response.uploadPortList), response.uploadIpList.zip(response.uploadPortList),
resource, resource,

View File

@ -0,0 +1,194 @@
/*
* 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.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.GroupMessagePostSendEvent
import net.mamoe.mirai.event.events.GroupMessagePreSendEvent
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.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.createToGroup
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,
forceAsLongMessage: Boolean,
): MessageReceipt<Group> {
val chain = transformedMessage
.transformSpecialMessages(this)
.convertToLongMessageIfNeeded(originalMessage, forceAsLongMessage, this)
chain.findIsInstance<QuoteReply>()?.source?.ensureSequenceIdAvailable()
chain.asSequence().filterIsInstance<FriendImage>().forEach { image ->
updateFriendImageForGroupMessage(image)
}
return sendMessagePacket(
originalMessage,
chain,
allowResendAsLongMessage = transformedMessage.takeSingleContent<LongMessageInternal>() == null
)
}
/**
* Called only in 'public' apis.
*/
internal suspend fun GroupImpl.broadcastGroupMessagePreSendEvent(message: Message): MessageChain {
return kotlin.runCatching {
GroupMessagePreSendEvent(this, message).broadcast()
}.onSuccess {
check(!it.isCancelled) {
throw EventCancelledException("cancelled by GroupMessagePreSendEvent")
}
}.getOrElse {
throw EventCancelledException("exception thrown when broadcasting GroupMessagePreSendEvent", it)
}.message.toMessageChain()
}
/**
* - [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()
}
/**
* Final process
*/
private suspend fun GroupImpl.sendMessagePacket(
originalMessage: Message,
finalMessage: MessageChain,
allowResendAsLongMessage: Boolean,
): MessageReceipt<Group> {
val group = this
val result = bot.network.runCatching sendMsg@{
val source: OnlineMessageSourceToGroupImpl
MessageSvcPbSendMsg.createToGroup(bot.client, group, finalMessage) {
source = it
}.sendAndExpect<MessageSvcPbSendMsg.Response>().let { resp ->
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
if (allowResendAsLongMessage) {
return@sendMsg sendMessageImpl(originalMessage, finalMessage, true)
} 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"
}
}
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
)
}
MessageReceipt(source, group)
}
GroupMessagePostSendEvent(this, finalMessage, result.exceptionOrNull(), result.getOrNull()).broadcast()
return result.getOrThrow()
}
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(
originalMessage: Message,
forceAsLongMessage: Boolean,
groupImpl: GroupImpl,
): MessageChain {
if (forceAsLongMessage || this.shouldSendAsLongMessage(originalMessage, groupImpl)) {
val resId = groupImpl.uploadGroupLongMessageHighway(this)
return this + RichMessage.longMessage(
brief = takeContent(27),
resId = resId,
timeSeconds = currentTimeSeconds()
) // LongMessageInternal replaces all contents and preserves metadata
}
return 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>()
}
}
private fun MessageChain.shouldSendAsLongMessage(originalMessage: Message, target: Contact): Boolean {
val length = verityLength(originalMessage, target)
return length > 700 || countImages() > 1
}

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.internal.asQQAndroidBot import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.message.LongMessageInternal
import net.mamoe.mirai.internal.message.OnlineMessageSourceToFriendImpl import net.mamoe.mirai.internal.message.OnlineMessageSourceToFriendImpl
import net.mamoe.mirai.internal.message.OnlineMessageSourceToStrangerImpl import net.mamoe.mirai.internal.message.OnlineMessageSourceToStrangerImpl
import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable
@ -26,6 +27,7 @@ import net.mamoe.mirai.internal.utils.estimateLength
import net.mamoe.mirai.message.* import net.mamoe.mirai.message.*
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.cast import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.verbose import net.mamoe.mirai.utils.verbose
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@ -52,7 +54,7 @@ internal suspend fun <T : User> Friend.sendMessageImpl(
}.getOrElse { }.getOrElse {
throw EventCancelledException("exception thrown when broadcasting FriendMessagePreSendEvent", it) throw EventCancelledException("exception thrown when broadcasting FriendMessagePreSendEvent", it)
}.message.toMessageChain() }.message.toMessageChain()
chain.verityLength(message, this, {}, {}) chain.verityLength(message, this)
chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable() chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
@ -104,7 +106,7 @@ internal suspend fun <T : User> Stranger.sendMessageImpl(
}.getOrElse { }.getOrElse {
throw EventCancelledException("exception thrown when broadcasting StrangerMessagePreSendEvent", it) throw EventCancelledException("exception thrown when broadcasting StrangerMessagePreSendEvent", it)
}.message.toMessageChain() }.message.toMessageChain()
chain.verityLength(message, this, {}, {}) chain.verityLength(message, this)
chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable() chain.firstIsInstanceOrNull<QuoteReply>()?.source?.ensureSequenceIdAvailable()
@ -138,32 +140,27 @@ internal suspend fun <T : User> Stranger.sendMessageImpl(
} }
internal fun Contact.logMessageSent(message: Message) { internal fun Contact.logMessageSent(message: Message) {
if (message !is LongMessage) { if (message !is LongMessageInternal) {
bot.logger.verbose("$this <- $message".replaceMagicCodes()) bot.logger.verbose("$this <- $message".replaceMagicCodes())
} }
} }
internal inline fun MessageChain.verityLength( internal fun MessageChain.countImages(): Int = this.count { it is Image }
message: Message, target: Contact,
lengthCallback: (Int) -> Unit,
imageCntCallback: (Int) -> Unit
) {
contract {
callsInPlace(lengthCallback, InvocationKind.EXACTLY_ONCE)
callsInPlace(imageCntCallback, InvocationKind.EXACTLY_ONCE)
}
internal fun MessageChain.verityLength(
originalMessage: Message, target: Contact,
): Int {
val chain = this val chain = this
val length = estimateLength(target, 15001) val length = estimateLength(target, 15001)
lengthCallback(length) if (length > 15000 || countImages() > 50) {
if (length > 15000 || count { it is Image }.apply { imageCntCallback(this) } > 50) {
throw MessageTooLargeException( throw MessageTooLargeException(
target, message, this, target, originalMessage, this,
"message(${ "message(${
chain.joinToString("", limit = 10) chain.joinToString("", limit = 10)
}) is too large. Allow up to 50 images or 5000 chars" }) is too large. Allow up to 50 images or 5000 chars"
) )
} }
return length
} }
@Suppress("RemoveRedundantQualifierName") // compiler bug @Suppress("RemoveRedundantQualifierName") // compiler bug
@ -214,3 +211,11 @@ internal fun String.applyCharMapping() = buildString(capacity = this.length) {
internal fun String.replaceMagicCodes(): String = this internal fun String.replaceMagicCodes(): String = this
.applyCharMapping() .applyCharMapping()
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()
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019-2021 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.message
import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey
import net.mamoe.mirai.message.data.AbstractServiceMessage
import net.mamoe.mirai.message.data.ServiceMessage
import net.mamoe.mirai.utils.safeCast
// internal runtime value, not serializable
internal data class LongMessageInternal internal constructor(override val content: String, val resId: String) :
AbstractServiceMessage() {
override val serviceId: Int get() = 35
companion object Key :
AbstractPolymorphicMessageKey<ServiceMessage, LongMessageInternal>(ServiceMessage, { it.safeCast() })
}
// internal runtime value, not serializable
internal data class ForwardMessageInternal(override val content: String) : AbstractServiceMessage() {
override val serviceId: Int get() = 35
companion object Key :
AbstractPolymorphicMessageKey<ServiceMessage, ForwardMessageInternal>(ServiceMessage, { it.safeCast() })
}

View File

@ -72,7 +72,7 @@ internal fun MessageChain.toRichTextElems(
) )
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
} }
is LongMessage -> { is LongMessageInternal -> {
check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" } check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
elements.add( elements.add(
ImMsgBody.Elem( ImMsgBody.Elem(
@ -375,7 +375,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
return buildMessageChain(initialSize = this.count()) { return buildMessageChain(initialSize = this.count()) {
this@cleanupRubbishMessageElements.forEach { element -> this@cleanupRubbishMessageElements.forEach { element ->
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
if (last is LongMessage && element is PlainText) { if (last is LongMessageInternal && element is PlainText) {
if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
previousLast = last previousLast = last
last = element last = element
@ -535,7 +535,7 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(
1 -> @Suppress("DEPRECATION_ERROR") 1 -> @Suppress("DEPRECATION_ERROR")
list.add(SimpleServiceMessage(1, content)) list.add(SimpleServiceMessage(1, content))
/** /**
* [LongMessage], [ForwardMessage] * [LongMessageInternal], [ForwardMessage]
*/ */
35 -> { 35 -> {
val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.longTextResid val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.longTextResid

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019-2021 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.network.highway
import kotlinx.io.core.Closeable
import net.mamoe.mirai.utils.runBIO
import net.mamoe.mirai.utils.withUse
import java.io.InputStream
internal class ChunkedFlowSession<T>(
private val input: InputStream,
private val buffer: ByteArray,
private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
) : Closeable {
override fun close() {
input.close()
}
private var offset = 0L
@Suppress("BlockingMethodInNonBlockingContext")
internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
runBIO {
while (true) {
val size = input.read(buffer)
if (size == -1) return@runBIO
block(mapper(buffer, size, offset))
offset += size
}
}
}
}

View File

@ -15,7 +15,6 @@ import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.* import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@ -34,7 +33,6 @@ import net.mamoe.mirai.internal.utils.toIpV4AddressString
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import java.io.InputStream import java.io.InputStream
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime import kotlin.time.measureTime
@ -83,10 +81,9 @@ internal suspend fun HttpClient.postImage(
} == HttpStatusCode.OK } == HttpStatusCode.OK
internal object HighwayHelper { internal object Highway {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@OptIn(ExperimentalTime::class) suspend fun uploadResource(
suspend fun uploadImageToServers(
bot: QQAndroidBot, bot: QQAndroidBot,
servers: List<Pair<Int, Int>>, servers: List<Pair<Int, Int>>,
uKey: ByteArray, uKey: ByteArray,
@ -104,7 +101,7 @@ internal object HighwayHelper {
} }
val time = measureTime { val time = measureTime {
uploadImage( uploadResourceImpl(
client = bot.client, client = bot.client,
serverIp = ip, serverIp = ip,
serverPort = port, serverPort = port,
@ -120,9 +117,7 @@ internal object HighwayHelper {
} }
} }
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private suspend fun uploadResourceImpl(
@OptIn(InternalCoroutinesApi::class)
internal suspend fun uploadImage(
client: QQAndroidClient, client: QQAndroidClient,
serverIp: String, serverIp: String,
serverPort: Int, serverPort: Int,
@ -213,30 +208,6 @@ internal object HighwayHelper {
} }
} }
internal class ChunkedFlowSession<T>(
private val input: InputStream,
private val buffer: ByteArray,
private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T
) : Closeable {
override fun close() {
input.close()
}
private var offset = 0L
@Suppress("BlockingMethodInNonBlockingContext")
internal suspend inline fun useAll(crossinline block: suspend (T) -> Unit) = withUse {
runBIO {
while (true) {
val size = input.read(buffer)
if (size == -1) return@runBIO
block(mapper(buffer, size, offset))
offset += size
}
}
}
}
internal fun createImageDataPacketSequence( internal fun createImageDataPacketSequence(
// RequestDataTrans // RequestDataTrans

View File

@ -102,7 +102,7 @@ internal class MultiMsg {
if (PacketLogger.isEnabled) { if (PacketLogger.isEnabled) {
return _miraiContentToString() return _miraiContentToString()
} }
return "MultiMsg.ApplyUp.Response.RequireUpload(proto=$proto)" return "MultiMsg.ApplyUp.Response.RequireUpload"
} }
} }
@ -110,7 +110,7 @@ internal class MultiMsg {
} }
// captured from group // captured from group
fun createForGroupLongMessage( fun createForGroup(
buType: Int, buType: Int,
client: QQAndroidClient, client: QQAndroidClient,
messageData: MessageValidationData, messageData: MessageValidationData,

View File

@ -295,7 +295,6 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
client: QQAndroidClient, client: QQAndroidClient,
targetGroup: Group, targetGroup: Group,
message: MessageChain, message: MessageChain,
isForward: Boolean,
source: OnlineMessageSourceToGroupImpl source: OnlineMessageSourceToGroupImpl
): OutgoingPacket = buildOutgoingUniPacket(client) { ): 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()) ///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())
@ -333,9 +332,10 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
msgRand = source.internalIds.single(), msgRand = source.internalIds.single(),
syncCookie = EMPTY_BYTE_ARRAY, syncCookie = EMPTY_BYTE_ARRAY,
msgVia = 1, msgVia = 1,
msgCtrl = if (isForward) MsgCtrl.MsgCtrl( msgCtrl =
msgFlag = 4 if (message[ForwardMessageInternal] != null)
) else null MsgCtrl.MsgCtrl(msgFlag = 4)
else null
) )
) )
} }
@ -429,7 +429,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup(
client: QQAndroidClient, client: QQAndroidClient,
group: Group, group: Group,
message: MessageChain, message: MessageChain,
isForward: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit
): OutgoingPacket { ): OutgoingPacket {
contract { contract {
@ -457,7 +456,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup(
client, client,
group, group,
message, message,
isForward,
source source
) )
} }