diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt index 5c6e8ceea..2aff75912 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt @@ -33,18 +33,20 @@ import net.mamoe.mirai.message.data.* import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl import net.mamoe.mirai.qqandroid.contact.QQImpl import net.mamoe.mirai.qqandroid.contact.checkIsGroupImpl +import net.mamoe.mirai.qqandroid.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend import net.mamoe.mirai.qqandroid.message.OnlineFriendImageImpl import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.qqandroid.network.QQAndroidClient -import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl -import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.MultiMsg -import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc -import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement +import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper +import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg +import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.* import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList +import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.io.encodeToString +import net.mamoe.mirai.utils.io.toReadPacket import kotlin.collections.asSequence import kotlin.coroutines.CoroutineContext import kotlin.math.absoluteValue @@ -367,27 +369,95 @@ internal abstract class QQAndroidBotBase constructor( @LowLevelAPI @MiraiExperimentalAPI override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) { + val chain = message.asMessageChain() + check(chain.toString().length <= 3000 && chain.count { it is Image } <= 10) { "message is too large" } + val group = getGroup(groupCode) + val source = MessageSourceFromSendFriend( messageRandom = Random.nextInt().absoluteValue, senderId = client.uin, toUin = Group.calculateGroupUinByGroupCode(groupCode), time = currentTimeSeconds, groupId = groupCode, - originalMessage = message.asMessageChain(), - sequenceId = 0 + originalMessage = chain, + sequenceId = client.atomicNextMessageSequenceId() // sourceMessage = message ) // TODO: 2020/3/26 util 方法来添加单例元素 - val toSend = buildMessageChain { - source.originalMessage.filter { it !is MessageSource }.forEach { - add(it) + val toSend = buildMessageChain(chain) { + source.originalMessage.forEach { + if (it !is MessageSource){ + add(it) + } } add(source) } + network.run { - val response = MultiMsg.ApplyUp.createForLongMessage(this@QQAndroidBotBase.client, toSend, groupCode) - .sendAndExpect() + val data = toSend.calculateValidationDataForGroup(group) + + val response = + MultiMsg.ApplyUp.createForLongMessage( + client = this@QQAndroidBotBase.client, + messageData = data, + dstUin = Group.calculateGroupUinByGroupCode(groupCode) + ).sendAndExpect() + + val resId: String + when (response) { + is MultiMsg.ApplyUp.Response.MessageTooLarge -> + error("message is too large") + is MultiMsg.ApplyUp.Response.OK -> { + resId = response.resId + } + 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 = Group.calculateGroupUinByGroupCode(groupCode), + msgId = 0, + msgUkey = response.proto.msgUkey, + needCache = 0, + storeType = 2, + msgContent = data.data + ) + ) + ).toByteArray(LongMsg.ReqBody.serializer()) + + HighwayHelper.uploadImage( + client, + serverIp = response.proto.uint32UpIp!!.first().toIpV4AddressString(), + serverPort = response.proto.uint32UpPort!!.first(), + ticket = response.proto.msgSig, // 104 + imageInput = body.toReadPacket(), + inputSize = body.size, + fileMd5 = MiraiPlatformUtils.md5(body), + commandId = 27 // long msg + ) + } + } + + group.sendMessage( + RichMessage.longMessage( + brief = toSend.joinToString { + when (it) { + is PlainText -> it.stringValue + is At -> it.toString() + else -> "" + } + }, + resId = resId, + timeSeconds = source.time + ) + ) + println(response._miraiContentToString()) } } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt index 730919295..2f66dcc24 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt @@ -65,6 +65,8 @@ internal class GroupImpl( companion object; override val bot: QQAndroidBot by bot.unsafeWeakRef() + + @OptIn(LowLevelAPI::class) val uin: Long = groupInfo.uin override lateinit var owner: Member @@ -289,8 +291,9 @@ internal class GroupImpl( source.startWaitingSequenceId(this) }.sendAndExpect() if (response is MessageSvc.PbSendMsg.Response.Failed) { - when (response.errorCode) { + when (response.resultType) { 120 -> error("bot is being muted.") + 34 -> error("internal error: send message failed, illegal arguments: $response") else -> error("send message failed: $response") } } @@ -352,7 +355,7 @@ internal class GroupImpl( imageInput = image.input, inputSize = image.inputSize.toInt(), fileMd5 = image.md5, - uKey = response.uKey, + ticket = response.uKey, commandId = 2 ) } ?: error("timeout uploading image: ${image.filename}") diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt index 0d11ee9aa..aff7d4f7d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt @@ -234,11 +234,25 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList { + check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" } + elements.add( + ImMsgBody.Elem( + richMsg = ImMsgBody.RichMsg( + serviceId = 35, // ok + template1 = byteArrayOf(1) + content + ) + ) + ) + transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) + longTextResId = it.resId + } is LightApp -> elements.add( ImMsgBody.Elem( lightApp = ImMsgBody.LightAppElem( @@ -246,24 +260,13 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList { - elements.add( - ImMsgBody.Elem( - richMsg = ImMsgBody.RichMsg( - serviceId = 35, - template1 = byteArrayOf(1) + content - ) - ) - ) - transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN) // required - } else -> elements.add( ImMsgBody.Elem( richMsg = ImMsgBody.RichMsg( serviceId = when (it) { is XmlMessage -> 60 is JsonMessage -> 1 - is MergedForwardedMessage -> 35 + // is MergedForwardedMessage -> 35 else -> error("unsupported RichMessage: ${it::class.simpleName}") }, template1 = byteArrayOf(1) + content @@ -296,8 +299,9 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList { } @@ -306,10 +310,24 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList()) { - // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00 - elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()))) - } else elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()))) + when { + longTextResId != null -> { + elements.add( + ImMsgBody.Elem( + generalFlags = ImMsgBody.GeneralFlags( + longTextFlag = 1, + longTextResid = longTextResId!!, + pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes() + ), + ) + ) + } + this.any() -> { + // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00 + elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()))) + } + else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()))) + } return elements } @@ -402,7 +420,7 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain { last = element return@forEach } else { - if (last is MergedForwardedMessage && element is PlainText) { + if (last is LongMessage && element is PlainText) { if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { last = element return@forEach @@ -416,6 +434,15 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain { } } +internal inline fun Iterable<*>.firstIsInstance(): R { + this.forEach { + if (it is R) { + return it + } + } + throw NoSuchElementException("Collection contains no element matching the predicate.") +} + /* if (this.any()) { var removed = false @@ -464,7 +491,12 @@ internal fun List.joinToMessageChain(message: MessageChainBuilde when (it.richMsg.serviceId) { 1 -> message.add(JsonMessage(content)) 60 -> message.add(XmlMessage(content)) - 35 -> message.add(MergedForwardedMessage(content)) + 35 -> message.add( + LongMessage( + content, + this.firstIsInstance().longTextResid + ) + ) else -> { @Suppress("DEPRECATION") MiraiLogger.debug { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index dc7ffd736..dda1901bc 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -262,32 +262,32 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler bot.groups.delegate.addLast( @Suppress("DuplicatedCode") (GroupImpl( - bot = bot, - coroutineContext = bot.coroutineContext, - id = troopNum.groupCode, - groupInfo = bot._lowLevelQueryGroupInfo(troopNum.groupCode).apply { - this as GroupInfoImpl + bot = bot, + coroutineContext = bot.coroutineContext, + id = troopNum.groupCode, + groupInfo = bot._lowLevelQueryGroupInfo(troopNum.groupCode).apply { + this as GroupInfoImpl - if (this.delegate.groupName == null) { - this.delegate.groupName = troopNum.groupName - } + if (this.delegate.groupName == null) { + this.delegate.groupName = troopNum.groupName + } - if (this.delegate.groupMemo == null) { - this.delegate.groupMemo = troopNum.groupMemo - } + if (this.delegate.groupMemo == null) { + this.delegate.groupMemo = troopNum.groupMemo + } - if (this.delegate.groupUin == null) { - this.delegate.groupUin = troopNum.groupUin - } + if (this.delegate.groupUin == null) { + this.delegate.groupUin = troopNum.groupUin + } - this.delegate.groupCode = troopNum.groupCode - }, - members = bot._lowLevelQueryGroupMemberList( - troopNum.groupUin, - troopNum.groupCode, - troopNum.dwGroupOwnerUin - ) - )) + this.delegate.groupCode = troopNum.groupCode + }, + members = bot._lowLevelQueryGroupMemberList( + troopNum.groupUin, + troopNum.groupCode, + troopNum.dwGroupOwnerUin + ) + )) ) }?.let { logger.error { "群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" } @@ -581,32 +581,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E { - val result = async { - withTimeoutOrNull(3000) { - withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { - PacketLogger.debug { "Channel sending: $commandName" } - when (data) { - is ByteArray -> channel.send(data, 0, length) - is ByteReadPacket -> channel.send(data) - else -> error("Internal error: unexpected data type: ${data::class.simpleName}") - } - PacketLogger.debug { "Channel send done: $commandName" } + withTimeoutOrNull(3000) { + withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { + PacketLogger.debug { "Channel sending: $commandName" } + when (data) { + is ByteArray -> channel.send(data, 0, length) + is ByteReadPacket -> channel.send(data) + else -> error("Internal error: unexpected data type: ${data::class.simpleName}") } - } ?: return@async "timeout sending packet $commandName" + PacketLogger.debug { "Channel send done: $commandName" } + } + } ?: throw TimeoutException("timeout sending packet $commandName") - logger.verbose("Send done: $commandName") - - withTimeoutOrNull(timeoutMillis) { - handler.await() - // 不要 `withTimeout`. timeout 的报错会不正常. - } ?: return@async "timeout receiving response of $commandName" - } + logger.verbose("Send done: $commandName") @Suppress("UNCHECKED_CAST") - when (val value = result.await()) { - is String -> throw TimeoutException(value) - else -> return value as E - } + return withTimeoutOrNull(timeoutMillis) { + handler.await() + // 不要 `withTimeout`. timeout 的报错会不正常. + } as E? ?: throw TimeoutException("timeout receiving response of $commandName") } if (retry == 0) { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt index 6d0aeccbd..da22f797b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt @@ -115,6 +115,9 @@ internal open class QQAndroidClient( private val highwayDataTransSequenceIdForFriend: AtomicInt = atomic(43973) internal fun nextHighwayDataTransSequenceIdForFriend(): Int = highwayDataTransSequenceIdForFriend.getAndAdd(2) + private val highwayDataTransSequenceIdForApplyUp: AtomicInt = atomic(77918) + internal fun nextHighwayDataTransSequenceIdForApplyUp(): Int = highwayDataTransSequenceIdForApplyUp.getAndAdd(2) + val appClientVersion: Int = 0 var networkType: NetworkType = NetworkType.WIFI diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt index 02989aeb0..d913d4431 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt @@ -101,7 +101,7 @@ internal object HighwayHelper { client: QQAndroidClient, serverIp: String, serverPort: Int, - uKey: ByteArray, + ticket: ByteArray, imageInput: Any, inputSize: Int, fileMd5: ByteArray, @@ -109,8 +109,8 @@ internal object HighwayHelper { ) { require(imageInput is Input || imageInput is InputStream || imageInput is ByteReadChannel) { "unsupported imageInput: ${imageInput::class.simpleName}" } require(fileMd5.size == 16) { "bad md5. Required size=16, got ${fileMd5.size}" } - require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" } - require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" } + // require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" } + // require(commandId == 2 || commandId == 1) { "bad commandId. Must be 1 or 2" } val socket = PlatformSocket() socket.connect(serverIp, serverPort) @@ -119,7 +119,7 @@ internal object HighwayHelper { client = client, command = "PicUp.DataUp", commandId = commandId, - uKey = uKey, + ticket = ticket, data = imageInput, dataSize = inputSize, fileMd5 = fileMd5 diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt index 64c570031..9f38d8f7a 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/highway.kt @@ -36,7 +36,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans dataFlag: Int = 4096, commandId: Int, localId: Int = 2052, - uKey: ByteArray, + ticket: ByteArray, data: Any, dataSize: Int, @@ -45,7 +45,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans ): Flow { ByteArrayPool.checkBufferSize(sizePerPacket) require(data is Input || data is InputStream || data is ByteReadChannel) { "unsupported data: ${data::class.simpleName}" } - require(uKey.size == 128) { "bad uKey. Required size=128, got ${uKey.size}" } + // require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" } require(data !is ByteReadPacket || data.remaining.toInt() == dataSize) { "bad input. given dataSize=$dataSize, but actual readRemaining=${(data as ByteReadPacket).remaining}" } val flow = when (data) { @@ -64,8 +64,12 @@ internal fun createImageDataPacketSequence( // RequestDataTrans version = 1, uin = client.uin.toString(), command = command, - seq = if (commandId == 2) client.nextHighwayDataTransSequenceIdForGroup() - else client.nextHighwayDataTransSequenceIdForFriend(), + seq = when (commandId) { + 2 -> client.nextHighwayDataTransSequenceIdForGroup() + 1 -> client.nextHighwayDataTransSequenceIdForFriend() + 27 -> client.nextHighwayDataTransSequenceIdForApplyUp() + else -> error("illegal commandId: $commandId") + }, retryTimes = 0, appid = appId, dataflag = dataFlag, @@ -77,7 +81,7 @@ internal fun createImageDataPacketSequence( // RequestDataTrans datalength = chunkedInput.bufferSize, dataoffset = offset, filesize = dataSize.toLong(), - serviceticket = uKey, + serviceticket = ticket, md5 = MiraiPlatformUtils.md5(chunkedInput.buffer, 0, chunkedInput.bufferSize), fileMd5 = fileMd5, flag = 0, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt index 8b9db91a0..82ae61362 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt @@ -17,7 +17,7 @@ import net.mamoe.mirai.qqandroid.io.ProtoBuf import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY @Serializable -class BdhExtinfo : ProtoBuf { +internal class BdhExtinfo : ProtoBuf { @Serializable class CommFileExtReq( @ProtoId(1) val actionType: Int = 0, @@ -140,7 +140,7 @@ class BdhExtinfo : ProtoBuf { } @Serializable -class CSDataHighwayHead : ProtoBuf { +internal class CSDataHighwayHead : ProtoBuf { @Serializable class C2CCommonExtendinfo( @ProtoId(1) val infoId: Int = 0, @@ -283,7 +283,7 @@ class CSDataHighwayHead : ProtoBuf { } @Serializable -class HwConfigPersistentPB : ProtoBuf { +internal class HwConfigPersistentPB : ProtoBuf { @Serializable class HwConfigItemPB( @ProtoId(1) val ingKey: String = "", @@ -315,7 +315,7 @@ class HwConfigPersistentPB : ProtoBuf { } @Serializable -class HwSessionInfoPersistentPB : ProtoBuf { +internal class HwSessionInfoPersistentPB : ProtoBuf { @Serializable class HwSessionInfoPB( @ProtoId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY, @@ -324,7 +324,7 @@ class HwSessionInfoPersistentPB : ProtoBuf { } @Serializable -class Subcmd0x501 : ProtoBuf { +internal class Subcmd0x501 : ProtoBuf { @Serializable class ReqBody( @ProtoId(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt index 2d606cb12..4833a66ca 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt @@ -467,7 +467,7 @@ internal class ImMsgBody : ProtoBuf { @ProtoId(4) val rpId: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(5) val prpFold: Int = 0, @ProtoId(6) val longTextFlag: Int = 0, - @ProtoId(7) val longTextResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(7) val longTextResid: String = "", @ProtoId(8) val groupType: Int = 0, @ProtoId(9) val toUinFlag: Int = 0, @ProtoId(10) val glamourLevel: Int = 0, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt index f77a52c39..1127fd142 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt @@ -44,7 +44,7 @@ internal class MultiMsg : ProtoBuf { @Serializable class MultiMsgApplyUpRsp( @ProtoId(1) val result: Int = 0, - @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(2) val msgResid: String = "", @ProtoId(3) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY, @ProtoId(4) val uint32UpIp: List? = null, @ProtoId(5) val uint32UpPort: List? = null, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt index 245bb7496..32115c51d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt @@ -12,7 +12,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat import kotlinx.io.core.ByteReadPacket -import net.mamoe.mirai.Bot +import net.mamoe.mirai.contact.Group import net.mamoe.mirai.message.data.* import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf @@ -29,6 +29,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgTransmit import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MultiMsg import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory +import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiPlatformUtils @@ -44,8 +45,8 @@ internal class MessageValidationData @OptIn(MiraiInternalAPI::class) constructor } @OptIn(MiraiInternalAPI::class) -internal fun MessageChain.calculateValidationData( - bot: Bot +internal fun MessageChain.calculateValidationDataForGroup( + group: Group ): MessageValidationData { // top_package.akkv#method_42702 val source: MessageSource by this.orElse { error("internal error: calculateValidationData: cannot find MessageSource, chain=${this._miraiContentToString()}") } @@ -55,37 +56,39 @@ internal fun MessageChain.calculateValidationData( } val richTextElems = this.toRichTextElems(source is MessageSourceFromSendGroup) + .filterNot { it.generalFlags != null } val msgTransmit = MsgTransmit.PbMultiMsgTransmit( msg = listOf( MsgComm.Msg( msgHead = MsgComm.MsgHead( - fromUin = source.senderId, + fromUin = group.bot.uin, msgSeq = source.sequenceId, msgTime = source.time.toInt(), - msgUid = source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT + msgUid = 0x01000000000000000L or source.messageRandom.toLong(), // TODO: 2020/3/26 CHECK IT mutiltransHead = MsgComm.MutilTransHead( status = 0, msgId = 1 ), msgType = 82, // troop groupInfo = MsgComm.GroupInfo( - groupCode = source.toUin, - groupCard = bot.nick, + groupCode = group.id, + groupCard = "Cinnamon"// group.botAsMember.nameCard, // Cinnamon ), + isSrcMsg = false ), msgBody = ImMsgBody.MsgBody( richText = ImMsgBody.RichText( - elems = richTextElems + elems = richTextElems.toMutableList() ) - ) - ) + ), + ), ) ) val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) - return MessageValidationData(MiraiPlatformUtils.zip(bytes)) + return MessageValidationData(MiraiPlatformUtils.gzip(bytes)) } /* @@ -154,18 +157,27 @@ Packet 20:02:50 : =======================共有 0 个包======================= internal class MultiMsg { object ApplyUp : OutgoingPacketFactory("MultiMsg.ApplyUp") { - class Response( - val proto: MultiMsg.MultiMsgApplyUpRsp - ) : Packet + sealed class Response : Packet { + data class RequireUpload( + val proto: MultiMsg.MultiMsgApplyUpRsp + ) : Response() { + override fun toString(): String { + if (PacketLogger.isEnabled) { + return _miraiContentToString() + } + return "MultiMsg.ApplyUp.Response.RequireUpload(proto=$proto)" + } + } - fun createForLongMessage( - client: QQAndroidClient, - message: MessageChain, - dstUin: Long, // group uin - ): OutgoingPacket = createForLongMessage(client, message.calculateValidationData(client.bot), dstUin) + object MessageTooLarge : Response() + + data class OK( + val resId: String + ) : Response() + } // captured from group - private fun createForLongMessage( + fun createForLongMessage( client: QQAndroidClient, messageData: MessageValidationData, dstUin: Long // group uin @@ -173,21 +185,24 @@ internal class MultiMsg { writeProtoBuf( MultiMsg.ReqBody.serializer(), MultiMsg.ReqBody( - subcmd = 1, - termType = 5, - platformType = 9, - netType = 3, // wifi=3, wap=5 - buildVer = client.buildVer, buType = 1, + buildVer = "8.2.0.1296", multimsgApplyupReq = listOf( MultiMsg.MultiMsgApplyUpReq( applyId = 0, dstUin = dstUin, msgMd5 = messageData.md5, - msgSize = messageData.data.size.toLong(), + msgSize = messageData.data.size.toLong().also { + println("data.size = $it") + }, msgType = 3 // TODO 3 for group? - ) - ) + ), + ), + netType = 3, // wifi=3, wap=5 + platformType = 9, + subcmd = 1, + termType = 5, + reqChannelType = 0, ) ) } @@ -210,13 +225,18 @@ internal class MultiMsg { } */ override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { - val response = readProtoBuf(MultiMsg.MultiMsgApplyUpRsp.serializer()) - check(response.result == 0) { - kotlin.run { - println(response._miraiContentToString()) - }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" } + val body = readProtoBuf(MultiMsg.RspBody.serializer()) + val response = body.multimsgApplyupRsp!!.first() + return when (response.result) { + 0 -> Response.RequireUpload(response) + 193 -> Response.MessageTooLarge + //1 -> Response.OK(resId = response.msgResid) + else -> { + error(kotlin.run { + println(response._miraiContentToString()) + }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" }) + } } - return Response(response) } } } \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt index 73a93646c..531074246 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt @@ -126,7 +126,7 @@ internal class MessageSvc { object EmptyResponse : GetMsgSuccess(emptyList()) - @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class, FlowPreview::class) + @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class, FlowPreview::class, LowLevelAPI::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { // 00 00 01 0F 08 00 12 00 1A 34 08 FF C1 C4 F1 05 10 FF C1 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 8A CA 91 D1 0C 48 9B A5 BD 9B 0A 58 DE 9D 99 F8 08 60 1D 68 FF C1 C4 F1 05 70 00 20 02 2A 9D 01 08 F3 C1 C4 F1 05 10 A2 FF 8C F0 03 18 01 22 8A 01 0A 2A 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 A6 01 20 0B 28 AE F9 01 30 F4 C1 C4 F1 05 38 A7 E3 D8 D4 84 80 80 80 01 B8 01 CD B5 01 12 08 08 01 10 00 18 00 20 00 1A 52 0A 50 0A 27 08 00 10 F4 C1 C4 F1 05 18 A7 E3 D8 D4 04 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 08 0A 06 0A 04 4E 4D 53 4C 12 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 12 04 4A 02 08 00 30 01 2A 15 08 97 A2 C1 F1 05 10 95 A6 F5 E5 0C 18 01 30 01 40 01 48 81 01 2A 10 08 D3 F7 B5 F1 05 10 DD F1 92 B7 07 18 01 30 01 38 00 42 00 48 00 val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer()) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt index 2c6272f25..e3dc780ac 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt @@ -13,6 +13,7 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.SinceMirai import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -46,25 +47,33 @@ interface RichMessage : MessageContent { * * @param brief 消息内容纯文本, 显示在图片的前面 */ + @SinceMirai("0.31.0") + @OptIn(MiraiInternalAPI::class) @MiraiExperimentalAPI - fun longMessage(brief: String, resId: String, time: Long): XmlMessage { - val template = """ - - - - $brief… - - 点击查看完整消息 - - - - """ + fun longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage { + val limited: String = if (brief.length > 30) { + brief.take(30) + "…" + } else { + brief + } - return XmlMessage(template) + val template = """ + + + + $limited + + 点击查看完整消息 + + + + """.trimIndent() + + return LongMessage(template, resId) } @MiraiExperimentalAPI @@ -142,11 +151,12 @@ class XmlMessage constructor(override val content: String) : RichMessage { } /** - * 合并转发消息 + * 长消息 */ @SinceMirai("0.31.0") @MiraiExperimentalAPI -class MergedForwardedMessage(override val content: String) : RichMessage { +@MiraiInternalAPI +class LongMessage(override val content: String, val resId: String) : RichMessage { companion object Key : Message.Key // serviceId = 35 diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt index 49a691efe..4f170b68d 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.utils import net.mamoe.mirai.Bot import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.network.LoginFailedException import kotlin.coroutines.CoroutineContext import kotlin.jvm.JvmStatic @@ -18,10 +19,33 @@ import kotlin.jvm.JvmStatic * 验证码, 设备锁解决器 */ expect abstract class LoginSolver { + /** + * 处理图片验证码. + * 返回 null 以表示无法处理验证码, 将会刷新验证码或重试登录. + * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止 + * + * @throws LoginFailedException + */ abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? + /** + * 处理滑动验证码. + * 返回 null 以表示无法处理验证码, 将会刷新验证码或重试登录. + * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止 + * + * @throws LoginFailedException + * @return 验证码解决成功后获得的 ticket. + */ abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? + /** + * 处理不安全设备验证. + * 在处理完成后返回任意内容 (包含 `null`) 均视为处理成功. + * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止. + * + * @return 任意内容. 返回值保留以供未来更新. + * @throws LoginFailedException + */ abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? companion object { @@ -38,10 +62,12 @@ expect open class BotConfiguration() { * 日志记录器 */ var botLoggerSupplier: ((Bot) -> MiraiLogger) + /** * 网络层日志构造器 */ var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) + /** * 设备信息覆盖. 默认使用随机的设备信息. */ @@ -56,23 +82,28 @@ expect open class BotConfiguration() { * 心跳周期. 过长会导致被服务器断开连接. */ var heartbeatPeriodMillis: Long + /** * 每次心跳时等待结果的时间. * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响. */ var heartbeatTimeoutMillis: Long + /** * 心跳失败后的第一次重连前的等待时间. */ var firstReconnectDelayMillis: Long + /** * 重连失败后, 继续尝试的每次等待时间 */ var reconnectPeriodMillis: Long + /** * 最多尝试多少次重连 */ var reconnectionRetryTimes: Int + /** * 验证码处理器 */ diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt index c47f22772..2b6206171 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.utils.io -import kotlinx.io.core.IoBuffer -import kotlinx.io.pool.ObjectPool import kotlin.random.Random import kotlin.random.nextInt @@ -201,14 +199,4 @@ fun ByteArray.toUShort(): UShort = fun ByteArray.toInt(): Int = (this[0].toInt().and(255) shl 24) + (this[1].toInt().and(255) shl 16) + (this[2].toInt().and(255) shl 8) + (this[3].toInt().and( 255 - ) shl 0) - -/** - * 从 [IoBuffer.Pool] [borrow][ObjectPool.borrow] 一个 [IoBuffer] 然后将 [this] 写入. - * 注意回收 ([ObjectPool.recycle]) - */ -fun ByteArray.toIoBuffer( - offset: Int = 0, - length: Int = this.size - offset, - pool: ObjectPool = IoBuffer.Pool -): IoBuffer = pool.borrow().let { it.writeFully(this, offset, length); it } \ No newline at end of file + ) shl 0) \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt index 3c40e4a36..8ed919284 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt @@ -18,6 +18,7 @@ import kotlin.jvm.JvmName /** * 要求 [this] 最小为 [min]. */ +@PublishedApi internal fun Int.coerceAtLeastOrFail(min: Int): Int { require(this >= min) return this @@ -26,6 +27,7 @@ internal fun Int.coerceAtLeastOrFail(min: Int): Int { /** * 要求 [this] 最小为 [min]. */ +@PublishedApi internal fun Long.coerceAtLeastOrFail(min: Long): Long { require(this >= min) return this @@ -34,10 +36,12 @@ internal fun Long.coerceAtLeastOrFail(min: Long): Long { /** * 要求 [this] 最大为 [max]. */ +@PublishedApi internal fun Int.coerceAtMostOrFail(max: Int): Int = if (this >= max) error("value is greater than its expected maximum value $max") else this +@PublishedApi internal fun Long.coerceAtMostOrFail(max: Long): Long = if (this >= max) error("value is greater than its expected maximum value $max") else this \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt index f40840a37..1d9c96cbe 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt @@ -14,7 +14,7 @@ package net.mamoe.mirai.utils import io.ktor.client.HttpClient /** - * 时间戳 + * 时间戳. */ expect val currentTimeMillis: Long