diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt b/mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt similarity index 98% rename from mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt rename to mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt index b6b5a6a93..46c00f2ba 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt +++ b/mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.utils.cryptor +package net.mamoe.mirai.qqandroid.utils.cryptor import android.annotation.SuppressLint import net.mamoe.mirai.utils.MiraiInternalAPI 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 cf5ea1bf2..53c1e009c 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,24 @@ 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.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 +import kotlin.random.Random @OptIn(MiraiInternalAPI::class) internal expect class QQAndroidBot constructor( @@ -360,6 +366,97 @@ internal abstract class QQAndroidBotBase constructor( return json.parse(GroupActiveData.serializer(), rep) } + @LowLevelAPI + @MiraiExperimentalAPI + override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) { + val chain = message.asMessageChain() + check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "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 = chain, + sequenceId = client.atomicNextMessageSequenceId() + // sourceMessage = message + ) + + // TODO: 2020/3/26 util 方法来添加单例元素 + val toSend = buildMessageChain(chain.size) { + source.originalMessage.forEach { + if (it !is MessageSource) { + add(it) + } + } + add(source) + } + + network.run { + val data = toSend.calculateValidationDataForGroup(group) + + val response = + MultiMsg.ApplyUp.createForGroupLongMessage( + 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.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(limit = 30) { + when (it) { + is PlainText -> it.stringValue + is At -> it.toString() + else -> "" + } + }, + resId = resId, + timeSeconds = source.time + ) + ) + } + } + override suspend fun queryImageUrl(image: Image): String = when (image) { is OnlineFriendImageImpl -> image.originUrl is OnlineGroupImageImpl -> image.originUrl 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 00e838145..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 @@ -288,9 +290,13 @@ internal class GroupImpl( source = it source.startWaitingSequenceId(this) }.sendAndExpect() - check( - response is MessageSvc.PbSendMsg.Response.SUCCESS - ) { "send message failed: $response" } + if (response is MessageSvc.PbSendMsg.Response.Failed) { + 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") + } + } } return MessageReceipt(source, this, botAsMember) @@ -349,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/io/serialization/JceOld.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/JceOld.kt index 1acaa21e2..d3eb05a7b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/JceOld.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/JceOld.kt @@ -18,10 +18,8 @@ import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.internal.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule -import kotlinx.serialization.protobuf.ProtoId import net.mamoe.mirai.qqandroid.io.JceStruct import net.mamoe.mirai.qqandroid.io.ProtoBuf -import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.BYTE import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.DOUBLE import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.FLOAT @@ -39,7 +37,7 @@ import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.STRUCT_END import net.mamoe.mirai.qqandroid.io.serialization.jce.Jce.Companion.ZERO_TYPE import net.mamoe.mirai.qqandroid.io.serialization.jce.JceHead import net.mamoe.mirai.qqandroid.io.serialization.jce.JceId -import net.mamoe.mirai.utils.io.readString +import net.mamoe.mirai.qqandroid.utils.io.readString import net.mamoe.mirai.utils.io.toReadPacket @PublishedApi diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt index 595b41269..fdfb7f2f2 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/jce/JceInput.kt @@ -11,7 +11,7 @@ package net.mamoe.mirai.qqandroid.io.serialization.jce import kotlinx.io.core.* import net.mamoe.mirai.qqandroid.io.serialization.JceCharset -import net.mamoe.mirai.utils.io.readString +import net.mamoe.mirai.qqandroid.utils.io.readString /** diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt index 1b72418a1..48c06f82d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt @@ -25,7 +25,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.firstValue import net.mamoe.mirai.utils.io.read -import net.mamoe.mirai.utils.io.readPacketExact +import net.mamoe.mirai.qqandroid.utils.io.readPacketExact import net.mamoe.mirai.utils.io.toReadPacket import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt index 4b9be436e..c33a7543d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt @@ -254,7 +254,7 @@ internal class MessageSourceFromSendGroup( override val groupId: Long, override val originalMessage: MessageChain ) : MessageSourceFromSend() { - private lateinit var sequenceIdDeferred: Deferred + internal lateinit var sequenceIdDeferred: Deferred @OptIn(ExperimentalCoroutinesApi::class) override val id: Long 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 3c14f3c97..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 @@ -6,6 +6,7 @@ * * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file: OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalUnsignedTypes::class) package net.mamoe.mirai.qqandroid.message @@ -218,6 +219,8 @@ private val atAllData = ImMsgBody.Elem( ) ) +private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。") + @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class) internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList { val elements = mutableListOf() @@ -231,33 +234,54 @@ 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( + data = byteArrayOf(1) + content + ) + ) + ) + else -> elements.add( + ImMsgBody.Elem( + richMsg = ImMsgBody.RichMsg( + serviceId = when (it) { + is XmlMessage -> 60 + is JsonMessage -> 1 + // is MergedForwardedMessage -> 35 + else -> error("unsupported RichMessage: ${it::class.simpleName}") + }, + template1 = byteArrayOf(1) + content + ) + ) + ) + } + } + when (it) { is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue))) is At -> { elements.add(ImMsgBody.Elem(text = it.toJceData())) elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) } - is LightApp -> elements.add( - ImMsgBody.Elem( - lightApp = ImMsgBody.LightAppElem( - data = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray()) - ) - ) - ) - is RichMessage -> elements.add( - ImMsgBody.Elem( - richMsg = ImMsgBody.RichMsg( - serviceId = when (it) { - is XmlMessage -> 60 - is JsonMessage -> 1 - else -> error("unsupported RichMessage") - }, - template1 = byteArrayOf(1) + MiraiPlatformUtils.zip(it.content.toByteArray()) - ) - ) - ) is OfflineGroupImage -> elements.add(ImMsgBody.Elem(customFace = it.toJceData())) is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate)) is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate)) @@ -267,16 +291,18 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList { if (forGroup) { check(it is QuoteReplyToSend.ToGroup) { - "sending a quote to group using QuoteReplyToSend.ToFriend" + "sending a quote to group using QuoteReplyToSend.ToFriend is prohibited" } if (it.sender is Member) { transformOneMessage(it.createAt()) } - transformOneMessage(" ".toMessage()) + transformOneMessage(PlainText(" ")) } } - is QuoteReply, - is MessageSource -> { + is QuoteReply, // already transformed above + is MessageSource, // mirai only + is RichMessage, // already transformed above + -> { } else -> error("unsupported message type: ${it::class.simpleName}") @@ -284,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 } @@ -358,7 +398,7 @@ internal fun MsgComm.Msg.toMessageChain(): MessageChain { return buildMessageChain(elements.size + 1) { +MessageSourceFromMsg(delegate = this@toMessageChain) elements.joinToMessageChain(this) - }.removeAtIfHasQuoteReply() + }.cleanupRubbishMessageElements() } // These two functions are not identical, dont combine. @@ -369,11 +409,40 @@ internal fun ImMsgBody.SourceMsg.toMessageChain(): MessageChain { return buildMessageChain(elements.size + 1) { +MessageSourceFromServer(delegate = this@toMessageChain) elements.joinToMessageChain(this) - }.removeAtIfHasQuoteReply() + }.cleanupRubbishMessageElements() +} + +private fun MessageChain.cleanupRubbishMessageElements(): MessageChain { + var last: SingleMessage? = null + return buildMessageChain(initialSize = this.count()) { + this@cleanupRubbishMessageElements.forEach { element -> + if (last == null) { + last = element + return@forEach + } else { + if (last is LongMessage && element is PlainText) { + if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) { + last = element + return@forEach + } + } + } + + add(element) + last = element + } + } +} + +internal inline fun Iterable<*>.firstIsInstance(): R { + this.forEach { + if (it is R) { + return it + } + } + throw NoSuchElementException("Collection contains no element matching the predicate.") } -private fun MessageChain.removeAtIfHasQuoteReply(): MessageChain = - this /* if (this.any()) { var removed = false @@ -387,9 +456,6 @@ private fun MessageChain.removeAtIfHasQuoteReply(): MessageChain = }.asMessageChain() } else this*/ -@OptIn( - MiraiInternalAPI::class, ExperimentalUnsignedTypes::class, MiraiDebugAPI::class, LowLevelAPI::class -) internal fun List.joinToMessageChain(message: MessageChainBuilder) { this.forEach { when { @@ -425,6 +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( + 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..7241c58e4 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 @@ -39,8 +39,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.io.ByteArrayPool import net.mamoe.mirai.utils.io.PlatformSocket -import net.mamoe.mirai.utils.io.readPacketExact -import net.mamoe.mirai.utils.io.useBytes +import net.mamoe.mirai.qqandroid.utils.io.readPacketExact +import net.mamoe.mirai.qqandroid.utils.io.useBytes import kotlin.coroutines.CoroutineContext import kotlin.jvm.Volatile import kotlin.time.ExperimentalTime @@ -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 3abcf94ee..fda220a5c 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 @@ -22,9 +22,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger import net.mamoe.mirai.qqandroid.network.protocol.packet.Tlv import net.mamoe.mirai.qqandroid.utils.NetworkType +import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.cryptor.ECDH -import net.mamoe.mirai.utils.cryptor.TEA +import net.mamoe.mirai.qqandroid.utils.cryptor.TEA import net.mamoe.mirai.utils.io.* /* @@ -101,7 +101,7 @@ internal open class QQAndroidClient( var openAppId: Long = 715019303L val apkVersionName: ByteArray get() = "8.2.7".toByteArray() - val buildVer: String get() = "8.2.7.4410" + val buildVer: String get() = "8.2.7.4410" // 8.2.0.1296 private val messageSequenceId: AtomicInt = atomic(22911) internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2) @@ -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..03ab3af1a 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 @@ -33,7 +33,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.copyAndClose import net.mamoe.mirai.utils.io.ByteArrayPool import net.mamoe.mirai.utils.io.PlatformSocket -import net.mamoe.mirai.utils.io.withUse +import net.mamoe.mirai.qqandroid.utils.io.withUse import kotlinx.serialization.InternalSerializationApi @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) @@ -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/LongMsg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/LongMsg.kt new file mode 100644 index 000000000..73e88414a --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/LongMsg.kt @@ -0,0 +1,72 @@ +package net.mamoe.mirai.qqandroid.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoId +import net.mamoe.mirai.qqandroid.io.ProtoBuf +import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY + +internal class LongMsg : ProtoBuf { + @Serializable + class MsgDeleteReq( + @ProtoId(1) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(2) val msgType: Int = 0 + ) : ProtoBuf + + @Serializable + class MsgDeleteRsp( + @ProtoId(1) val result: Int = 0, + @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + class MsgDownReq( + @ProtoId(1) val srcUin: Int = 0, + @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(3) val msgType: Int = 0, + @ProtoId(4) val needCache: Int = 0 + ) : ProtoBuf + + @Serializable + class MsgDownRsp( + @ProtoId(1) val result: Int = 0, + @ProtoId(2) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(3) val msgContent: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + class MsgUpReq( + @ProtoId(1) val msgType: Int = 0, + @ProtoId(2) val dstUin: Long = 0L, + @ProtoId(3) val msgId: Int = 0, + @ProtoId(4) val msgContent: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(5) val storeType: Int = 0, + @ProtoId(6) val msgUkey: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(7) val needCache: Int = 0 + ) : ProtoBuf + + @Serializable + class MsgUpRsp( + @ProtoId(1) val result: Int = 0, + @ProtoId(2) val msgId: Int = 0, + @ProtoId(3) val msgResid: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + class ReqBody( + @ProtoId(1) val subcmd: Int = 0, + @ProtoId(2) val termType: Int = 0, + @ProtoId(3) val platformType: Int = 0, + @ProtoId(4) val msgUpReq: List? = null, + @ProtoId(5) val msgDownReq: List? = null, + @ProtoId(6) val msgDelReq: List? = null, + @ProtoId(10) val agentType: Int = 0 + ) : ProtoBuf + + @Serializable + class RspBody( + @ProtoId(1) val subcmd: Int = 0, + @ProtoId(2) val msgUpRsp: List? = null, + @ProtoId(3) val msgDownRsp: List? = null, + @ProtoId(4) val msgDelRsp: List? = null + ) : ProtoBuf +} \ No newline at end of file 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/MsgTransmit.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgTransmit.kt new file mode 100644 index 000000000..fcd15eb7c --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgTransmit.kt @@ -0,0 +1,25 @@ +package net.mamoe.mirai.qqandroid.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoId +import net.mamoe.mirai.qqandroid.io.ProtoBuf +import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY + +internal class MsgTransmit : ProtoBuf { + @Serializable + class PbMultiMsgItem( + @ProtoId(1) val fileName: String = "", + @ProtoId(2) val buffer: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + class PbMultiMsgNew( + @ProtoId(1) val msg: List? = null + ) : ProtoBuf + + @Serializable + class PbMultiMsgTransmit( + @ProtoId(1) val msg: List? = null, + @ProtoId(2) val pbItemList: List? = null + ) : ProtoBuf +} \ No newline at end of file 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 new file mode 100644 index 000000000..1127fd142 --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MultiMsg.kt @@ -0,0 +1,80 @@ +package net.mamoe.mirai.qqandroid.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoId +import net.mamoe.mirai.qqandroid.io.ProtoBuf +import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY + +@Serializable +internal class MultiMsg : ProtoBuf { + @Serializable + class ExternMsg( + @ProtoId(1) val channelType: Int = 0 + ) : ProtoBuf + + @Serializable + class MultiMsgApplyDownReq( + @ProtoId(1) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(2) val msgType: Int = 0, + @ProtoId(3) val srcUin: Long = 0L + ) : ProtoBuf + + @Serializable + class MultiMsgApplyDownRsp( + @ProtoId(1) val result: Int = 0, + @ProtoId(2) val thumbDownPara: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(3) val msgKey: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(4) val uint32DownIp: List? = null, + @ProtoId(5) val uint32DownPort: List? = null, + @ProtoId(6) val msgResid: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(7) val msgExternInfo: MultiMsg.ExternMsg? = null, + @ProtoId(8) val bytesDownIpV6: List? = null, + @ProtoId(9) val uint32DownV6Port: List? = null + ) : ProtoBuf + + @Serializable + class MultiMsgApplyUpReq( + @ProtoId(1) val dstUin: Long = 0L, + @ProtoId(2) val msgSize: Long = 0L, + @ProtoId(3) val msgMd5: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(4) val msgType: Int = 0, + @ProtoId(5) val applyId: Int = 0 + ) : ProtoBuf + + @Serializable + class MultiMsgApplyUpRsp( + @ProtoId(1) val result: Int = 0, + @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, + @ProtoId(6) val blockSize: Long = 0L, + @ProtoId(7) val upOffset: Long = 0L, + @ProtoId(8) val applyId: Int = 0, + @ProtoId(9) val msgKey: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(10) val msgSig: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoId(11) val msgExternInfo: MultiMsg.ExternMsg? = null, + @ProtoId(12) val bytesUpIpV6: List? = null, + @ProtoId(13) val uint32UpV6Port: List? = null + ) : ProtoBuf + + @Serializable + class ReqBody( + @ProtoId(1) val subcmd: Int = 0, + @ProtoId(2) val termType: Int = 0, + @ProtoId(3) val platformType: Int = 0, + @ProtoId(4) val netType: Int = 0, + @ProtoId(5) val buildVer: String = "", + @ProtoId(6) val multimsgApplyupReq: List? = null, + @ProtoId(7) val multimsgApplydownReq: List? = null, + @ProtoId(8) val buType: Int = 0, + @ProtoId(9) val reqChannelType: Int = 0 + ) : ProtoBuf + + @Serializable + class RspBody( + @ProtoId(1) val subcmd: Int = 0, + @ProtoId(2) val multimsgApplyupRsp: List? = null, + @ProtoId(3) val multimsgApplydownRsp: List? = null + ) : ProtoBuf +} diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt index fcafc4cda..f663a0f9a 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt @@ -14,10 +14,10 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.buildPacket import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.QQAndroidClient -import net.mamoe.mirai.utils.cryptor.ECDH -import net.mamoe.mirai.utils.cryptor.ECDHKeyPair -import net.mamoe.mirai.utils.io.encryptAndWrite -import net.mamoe.mirai.utils.io.writeShortLVByteArray +import net.mamoe.mirai.qqandroid.utils.cryptor.ECDH +import net.mamoe.mirai.qqandroid.utils.cryptor.ECDHKeyPair +import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite +import net.mamoe.mirai.qqandroid.utils.io.writeShortLVByteArray @OptIn(ExperimentalUnsignedTypes::class) internal interface EncryptMethod { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt index ef95247a5..1ae7d3289 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/OutgoingPacketAndroid.kt @@ -16,9 +16,9 @@ import kotlinx.io.core.buildPacket import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.utils.MiraiInternalAPI -import net.mamoe.mirai.utils.io.encryptAndWrite -import net.mamoe.mirai.utils.io.writeHex -import net.mamoe.mirai.utils.io.writeIntLVPacket +import net.mamoe.mirai.qqandroid.utils.io.encryptAndWrite +import net.mamoe.mirai.qqandroid.utils.io.writeHex +import net.mamoe.mirai.qqandroid.utils.io.writeIntLVPacket internal class OutgoingPacket constructor( name: String?, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt index 5ee2ded4a..4a11f2c1f 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt @@ -11,9 +11,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet import kotlinx.io.core.* import kotlinx.io.pool.useInstance -import net.mamoe.mirai.qqandroid.network.Packet import net.mamoe.mirai.event.Event import net.mamoe.mirai.qqandroid.QQAndroidBot +import net.mamoe.mirai.qqandroid.network.Packet +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.protocol.packet.chat.image.ImgStore @@ -26,9 +27,13 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin import net.mamoe.mirai.qqandroid.network.readUShortLVByteArray +import net.mamoe.mirai.qqandroid.utils.io.readPacketExact +import net.mamoe.mirai.qqandroid.utils.io.readString +import net.mamoe.mirai.qqandroid.utils.io.useBytes +import net.mamoe.mirai.qqandroid.utils.io.withUse import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.cryptor.TEA -import net.mamoe.mirai.utils.cryptor.adjustToPublicKey +import net.mamoe.mirai.qqandroid.utils.cryptor.TEA +import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey import net.mamoe.mirai.utils.io.* import kotlin.jvm.JvmName @@ -144,7 +149,8 @@ internal object KnownPacketFactories { TroopManagement.EditGroupNametag, TroopManagement.Kick, Heartbeat.Alive, - PbMessageSvc.PbMsgWithDraw + PbMessageSvc.PbMsgWithDraw, + MultiMsg.ApplyUp ) object IncomingFactories : List> by mutableListOf( diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt index 91e6ab2a5..ce54b8901 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt @@ -17,6 +17,7 @@ import kotlinx.io.core.toByteArray import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.protocol.LoginType import net.mamoe.mirai.qqandroid.utils.NetworkType +import net.mamoe.mirai.qqandroid.utils.io.* import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiPlatformUtils import net.mamoe.mirai.utils.currentTimeMillis 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 new file mode 100644 index 000000000..fbc332b7a --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt @@ -0,0 +1,238 @@ +/* + * 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 + */ + +@file:Suppress("EXPERIMENTAL_API_USAGE") + +package net.mamoe.mirai.qqandroid.network.protocol.packet.chat + +import kotlinx.io.core.ByteReadPacket +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 +import net.mamoe.mirai.qqandroid.io.serialization.toByteArray +import net.mamoe.mirai.qqandroid.io.serialization.writeProtoBuf +import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend +import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup +import net.mamoe.mirai.qqandroid.message.toRichTextElems +import net.mamoe.mirai.qqandroid.network.Packet +import net.mamoe.mirai.qqandroid.network.QQAndroidClient +import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm +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 +import net.mamoe.mirai.utils._miraiContentToString + +internal class MessageValidationData @OptIn(MiraiInternalAPI::class) constructor( + val data: ByteArray, + val md5: ByteArray = MiraiPlatformUtils.md5(data), +) { + override fun toString(): String { + return "MessageValidationData(data=, md5=${md5.contentToString()})" + } +} + +@OptIn(MiraiInternalAPI::class) +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()}") } + + check(source is MessageSourceFromSendGroup || source is MessageSourceFromSendFriend) { + "internal error: calculateValidationData: MessageSource must be " + } + + val richTextElems = this.toRichTextElems(source is MessageSourceFromSendGroup) + .filterNot { it.generalFlags != null } + + val msgTransmit = MsgTransmit.PbMultiMsgTransmit( + msg = listOf( + MsgComm.Msg( + msgHead = MsgComm.MsgHead( + fromUin = group.bot.uin, + msgSeq = source.sequenceId, + msgTime = source.time.toInt(), + 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 = group.id, + groupCard = "Cinnamon"// group.botAsMember.nameCard, // Cinnamon + ), + isSrcMsg = false + ), + msgBody = ImMsgBody.MsgBody( + richText = ImMsgBody.RichText( + elems = richTextElems.toMutableList() + ) + ), + ), + ) + ) + + val bytes = msgTransmit.toByteArray(MsgTransmit.PbMultiMsgTransmit.serializer()) + + return MessageValidationData(MiraiPlatformUtils.gzip(bytes)) +} + +/* + +=======================处理客户端到服务器======================= +flag1=0x0000000B(11), flag2=1, sequenceId = 00 00 E0 90, flag3=00, // 解密 bodyouter by D2 key +Packet 20:02:51 : ByteReadPacket outer body decrypted=00 00 00 28 00 00 00 18 4D 65 73 73 61 67 65 53 76 63 2E 50 62 53 65 6E 64 4D 73 67 00 00 00 08 8A 51 B1 25 00 00 00 04 00 00 02 7A 0A 08 12 06 08 F6 DD 96 FC 03 12 07 08 01 10 00 18 F2 46 1A D5 04 0A D2 04 12 A9 03 62 A6 03 0A A1 03 01 78 9C 7D 91 4B 4F DB 40 10 C7 BF CA 6A 2F 3E 81 1F 21 0D 91 6C 23 F1 48 15 CA A3 28 04 89 5C AA C5 1E 9B 15 6B 3B F5 AE 97 38 27 E8 05 04 5C B9 21 71 A1 95 B8 D0 1E 2A 24 54 F5 CB 20 25 F0 31 18 1B AA DE 90 46 AB 9D 9D D9 F9 CF 6F C6 5D 18 25 82 68 C8 25 CF 52 CF B0 67 2D 83 40 1A 64 21 4F 63 CF E8 6F 77 66 E6 0D 22 15 4B 43 26 B2 14 3C A3 04 69 90 05 DF 4D 64 4C 24 E4 9A 07 D0 5D F6 68 A3 49 89 82 64 28 98 AA 7D 9B 12 16 A8 AA 26 D5 1C 0E D7 0B A1 F8 BA 8C 29 D9 CB 39 44 18 77 1A 68 8E 8D 16 49 A9 A5 0E AB 83 E9 10 4D 6B 4A 92 2F 39 48 1E 7A 74 73 BB BD B1 16 7C B4 07 65 B9 37 E2 6D 4B 1F 9A 81 B5 3B B6 16 57 F9 4A F0 69 D5 71 3E 2C 2D F6 0F 76 3A 5D 25 C7 73 5B 03 D9 1F 8D 8B AF 83 9E 39 6A 44 9F 75 6F 4D 55 A5 22 2E 60 83 25 80 B2 CD F9 A6 DD 68 B5 5A 4E DB B6 28 91 59 91 07 80 7D 75 51 09 FD 22 17 1E A5 24 12 2C 46 24 44 08 71 32 3D 1E A7 75 34 79 83 E8 D4 61 9B FA 2E 47 64 22 58 99 15 EA F5 41 71 25 C0 7F 1F EE F1 E8 87 6B BE 26 BA FB 39 D9 E7 61 08 28 10 31 21 01 5B 52 A5 80 5A CE F4 5D 59 24 09 CB 4B FF E9 DB C3 E4 E4 CF F4 FA FB D3 D5 F9 E4 EE 62 7A F9 7B 7A 7F 3A 3D FE E9 9A FF 32 5C B3 EA 05 7F D4 44 24 AD 69 9F 8F CF 26 37 B7 CF 77 BF 26 7F 2F 29 E1 41 B5 8D FF 7B C1 DB 70 58 8D 78 C6 AE C5 4C 5C A9 FF 02 0F 69 BA A1 10 23 12 4D 0A 4B 0A 49 E4 BD A0 E7 9A 84 51 51 E6 9A 82 E4 B8 8D E6 94 AF E6 8C 81 E6 9F A5 E7 9C 8B 5B E8 BD AC E5 8F 91 E5 A4 9A E6 9D A1 E6 B6 88 E6 81 AF 5D EF BC 8C E8 AF B7 E6 9C 9F E5 BE 85 E5 90 8E E7 BB AD E7 89 88 E6 9C AC E3 80 82 12 55 AA 02 52 30 01 3A 40 4F 54 39 4E 4C 63 47 31 5A 79 79 62 78 69 39 30 76 77 2F 63 30 59 7A 30 42 4A 69 45 63 4B 4A 32 32 36 43 42 55 6B 56 46 49 74 73 7A 34 51 5A 73 55 78 7A 75 71 5A 53 2F 78 33 66 50 76 53 4C 74 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 92 3F 28 E9 9B 97 86 04 40 00 +// 尝试解 Uni +// head +Packet Debug 20:02:51 : head=00 00 00 18 4D 65 73 73 61 67 65 53 76 63 2E 50 62 53 65 6E 64 4D 73 67 00 00 00 08 8A 51 B1 25 00 00 00 04 +Packet 20:02:51 : commandName=MessageSvc.PbSendMsg +MessageSvc.PbSendMsg + unknown4Bytes=8A 51 B1 25 + extraData= +Packet Debug 20:02:51 : Real body=0A 08 12 06 08 F6 DD 96 FC 03 12 07 08 01 10 00 18 F2 46 1A D5 04 0A D2 04 12 A9 03 62 A6 03 0A A1 03 01 78 9C 7D 91 4B 4F DB 40 10 C7 BF CA 6A 2F 3E 81 1F 21 0D 91 6C 23 F1 48 15 CA A3 28 04 89 5C AA C5 1E 9B 15 6B 3B F5 AE 97 38 27 E8 05 04 5C B9 21 71 A1 95 B8 D0 1E 2A 24 54 F5 CB 20 25 F0 31 18 1B AA DE 90 46 AB 9D 9D D9 F9 CF 6F C6 5D 18 25 82 68 C8 25 CF 52 CF B0 67 2D 83 40 1A 64 21 4F 63 CF E8 6F 77 66 E6 0D 22 15 4B 43 26 B2 14 3C A3 04 69 90 05 DF 4D 64 4C 24 E4 9A 07 D0 5D F6 68 A3 49 89 82 64 28 98 AA 7D 9B 12 16 A8 AA 26 D5 1C 0E D7 0B A1 F8 BA 8C 29 D9 CB 39 44 18 77 1A 68 8E 8D 16 49 A9 A5 0E AB 83 E9 10 4D 6B 4A 92 2F 39 48 1E 7A 74 73 BB BD B1 16 7C B4 07 65 B9 37 E2 6D 4B 1F 9A 81 B5 3B B6 16 57 F9 4A F0 69 D5 71 3E 2C 2D F6 0F 76 3A 5D 25 C7 73 5B 03 D9 1F 8D 8B AF 83 9E 39 6A 44 9F 75 6F 4D 55 A5 22 2E 60 83 25 80 B2 CD F9 A6 DD 68 B5 5A 4E DB B6 28 91 59 91 07 80 7D 75 51 09 FD 22 17 1E A5 24 12 2C 46 24 44 08 71 32 3D 1E A7 75 34 79 83 E8 D4 61 9B FA 2E 47 64 22 58 99 15 EA F5 41 71 25 C0 7F 1F EE F1 E8 87 6B BE 26 BA FB 39 D9 E7 61 08 28 10 31 21 01 5B 52 A5 80 5A CE F4 5D 59 24 09 CB 4B FF E9 DB C3 E4 E4 CF F4 FA FB D3 D5 F9 E4 EE 62 7A F9 7B 7A 7F 3A 3D FE E9 9A FF 32 5C B3 EA 05 7F D4 44 24 AD 69 9F 8F CF 26 37 B7 CF 77 BF 26 7F 2F 29 E1 41 B5 8D FF 7B C1 DB 70 58 8D 78 C6 AE C5 4C 5C A9 FF 02 0F 69 BA A1 10 23 12 4D 0A 4B 0A 49 E4 BD A0 E7 9A 84 51 51 E6 9A 82 E4 B8 8D E6 94 AF E6 8C 81 E6 9F A5 E7 9C 8B 5B E8 BD AC E5 8F 91 E5 A4 9A E6 9D A1 E6 B6 88 E6 81 AF 5D EF BC 8C E8 AF B7 E6 9C 9F E5 BE 85 E5 90 8E E7 BB AD E7 89 88 E6 9C AC E3 80 82 12 55 AA 02 52 30 01 3A 40 4F 54 39 4E 4C 63 47 31 5A 79 79 62 78 69 39 30 76 77 2F 63 30 59 7A 30 42 4A 69 45 63 4B 4A 32 32 36 43 42 55 6B 56 46 49 74 73 7A 34 51 5A 73 55 78 7A 75 71 5A 53 2F 78 33 66 50 76 53 4C 74 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 92 3F 28 E9 9B 97 86 04 40 00 +Packet 20:02:51 : ByteReadPacket uni packet= +Packet 20:02:51 : =======================共有 1 个包======================= + +=======================处理服务器到客户端客户端======================= + +=======================处理服务器到客户端客户端======================= +Packet 20:02:51 : ByteReadPacket 正在处理=00 00 00 0B 01 00 00 00 00 0E 31 39 39 34 37 30 31 30 32 31 0B 1C 26 01 C3 F8 46 01 ED 8D 1E C8 86 C1 62 89 9C F4 16 57 67 99 4F 39 E7 69 4F 74 33 6E 92 89 74 49 09 84 19 10 6F 3C 81 DA 0C 92 DD 04 B7 60 C9 DB C8 4F F8 60 57 A2 3F CF 95 5F 01 F8 0A 79 E7 28 B9 6A F6 AD 0A 71 BA 54 F8 8C DF AF CF D3 +Packet 20:02:51 : flag1(0A/0B) = 0B +Packet 20:02:51 : 包类型(flag2) = 1. (可能是 uni) +Packet 20:02:51 : 成功使用 d2Key 解密 +Packet 20:02:51 : ByteReadPacket sso/uni body==00 00 00 34 00 00 E0 90 00 00 00 00 00 00 00 04 00 00 00 18 4D 65 73 73 61 67 65 53 76 63 2E 50 62 53 65 6E 64 4D 73 67 00 00 00 08 8A 51 B1 25 00 00 00 00 00 00 00 0C 08 00 18 EA 90 ED F3 05 +Packet 20:02:51 : sequenceId = 57488 +Packet 20:02:51 : sso(inner)extraData = +Packet Debug 20:02:51 : commandName=MessageSvc.PbSendMsg +Packet 20:02:51 : 不是oicq response(可能是 UNI/PB)= 00 00 00 0C 08 00 18 EA 90 ED F3 05 +Packet 20:02:51 : =======================共有 0 个包======================= + + */ + +/* + +=======================处理客户端到服务器======================= +flag1=0x0000000B(11), flag2=1, sequenceId = 00 00 E0 8D, flag3=00, // 解密 bodyouter by D2 key +Packet 20:02:50 : ByteReadPacket outer body decrypted=00 00 00 24 00 00 00 14 4D 75 6C 74 69 4D 73 67 2E 41 70 70 6C 79 55 70 00 00 00 08 8A 51 B1 25 00 00 00 04 00 00 00 3B 08 01 10 05 18 09 20 03 2A 0A 38 2E 32 2E 30 2E 31 32 39 36 32 1F 08 F6 DD 96 FC 03 10 CF 05 1A 10 BB 45 B9 71 2C F4 D3 06 5D A7 A7 A2 FF D4 62 D2 20 03 28 00 40 01 +// 尝试解 Uni +// head +Packet Debug 20:02:50 : head=00 00 00 14 4D 75 6C 74 69 4D 73 67 2E 41 70 70 6C 79 55 70 00 00 00 08 8A 51 B1 25 00 00 00 04 +Packet 20:02:50 : commandName=MultiMsg.ApplyUp +MultiMsg.ApplyUp + unknown4Bytes=8A 51 B1 25 + extraData= +Packet Debug 20:02:50 : Real body=08 01 10 05 18 09 20 03 2A 0A 38 2E 32 2E 30 2E 31 32 39 36 32 1F 08 F6 DD 96 FC 03 10 CF 05 1A 10 BB 45 B9 71 2C F4 D3 06 5D A7 A7 A2 FF D4 62 D2 20 03 28 00 40 01 +Packet 20:02:50 : ByteReadPacket uni packet= +Packet 20:02:50 : =======================共有 1 个包======================= + +=======================处理服务器到客户端客户端======================= +Packet 20:02:50 : ByteReadPacket 正在处理=00 00 00 0B 01 00 00 00 00 0E 31 39 39 34 37 30 31 30 32 31 8A B2 8A B1 DA C9 60 28 D8 55 AB 39 B9 07 A6 D8 BA F2 55 87 C2 C9 29 08 53 CC AF 99 3F 22 26 1F 66 01 09 60 F2 2A 3C F1 A4 DC 74 5A 27 1C 47 E2 F0 7E 57 0C 9B 50 7D 0D 52 A3 17 BB B7 8D 9B 62 3A B3 E2 65 6D 7C 74 24 79 11 A5 23 78 83 63 35 8C C9 34 4A D9 CD 61 4D 0D 73 74 DF 49 F3 AD 65 2D 1A 87 14 2F 03 5F 0B 16 1F 87 CE 2A 53 3E 9F 8F CF 0F B8 C3 6B E1 6C 42 46 0D 59 F2 89 7E 8A 47 A8 CC 52 C0 E7 5C E4 CD 00 A0 00 61 FA AF 95 C1 C4 1B 8C C3 24 48 A5 4D 4F D7 59 38 F1 AE 4A 3B 18 7E 52 96 D5 2D 5D 67 D0 B8 0C BC F0 FD 3E 45 2C 7F 2E 1B AC FF F1 86 04 9B 8E 16 DF 7F C0 1C 25 13 36 21 D8 87 B1 FA BA 6E D2 DA E3 02 D2 31 45 9D 61 D4 43 07 F6 B5 D3 B0 6D 72 8B 83 FA B5 90 A7 BA 7A 32 2C 28 96 67 AC AB 42 37 EF 51 5B A1 A8 2D 17 93 F9 2C 22 51 6C 49 0A ED 38 AF 88 A1 E4 C7 09 BC DA 11 3F 46 DF D3 60 51 0E 92 89 56 D6 0D B4 66 DC 74 77 64 42 95 56 BE 89 61 75 CB F7 8C 33 D4 6B 40 4F 07 43 5B D9 A4 38 E1 DC 2A 0D 4D D6 8D 2B F5 E4 A2 45 3D EF 77 E5 24 F5 09 5E 1C 9C 14 CA 33 4D 3D 63 83 2E 38 94 13 1D 7A 0D 62 DB 89 0D 27 8D E2 58 5D 24 25 BC 9F D3 E3 3A 55 F2 FB 93 69 61 F0 25 E6 7F 7F B6 25 87 33 5B 5F 35 C1 E0 C4 6E 25 41 A0 12 B5 E6 DA 1A C9 F4 20 31 86 D3 B2 C9 D3 2D 96 40 92 BC BD 38 AD D6 94 E9 25 14 12 2D B6 32 6E D5 37 7D C6 E3 A8 E5 1E AD 97 52 FA DD CC 7E 96 5A E0 CB AF 79 4B CB BC E3 9F 57 4C 94 C7 9D 58 83 D0 11 41 BD E6 9C E1 98 7B BB 5B +Packet 20:02:50 : flag1(0A/0B) = 0B +Packet 20:02:50 : 包类型(flag2) = 1. (可能是 uni) +Packet 20:02:50 : 成功使用 d2Key 解密 +Packet 20:02:50 : ByteReadPacket sso/uni body==00 00 00 30 00 00 E0 8D 00 00 00 00 00 00 00 04 00 00 00 14 4D 75 6C 74 69 4D 73 67 2E 41 70 70 6C 79 55 70 00 00 00 08 8A 51 B1 25 00 00 00 00 00 00 01 88 08 01 12 FF 02 08 00 12 40 4F 54 39 4E 4C 63 47 31 5A 79 79 62 78 69 39 30 76 77 2F 63 30 59 7A 30 42 4A 69 45 63 4B 4A 32 32 36 43 42 55 6B 56 46 49 74 73 7A 34 51 5A 73 55 78 7A 75 71 5A 53 2F 78 33 66 50 76 53 4C 74 1A 98 01 1B 76 62 FB B2 C6 24 C3 1F 39 47 0D 45 5C 77 BD 0C 8F 69 FB C8 4F D8 76 83 26 60 EA A3 24 BC FD F6 C8 B4 64 DA 47 9D 6C 1A FA F4 EF 02 FC A4 76 1F 87 EB FF 51 62 20 E9 1F 74 6B 2F 7B 7C 53 EC 6D A2 53 AC 2B 93 B4 79 83 6D E6 D8 86 E1 D5 E2 4D EE 75 03 A3 3B 72 EB 0A 3E 13 3A 80 70 EF CC B4 0D F9 42 E3 DF 5F 7A 4C 36 BC 3B 9C 31 5A B1 40 B4 5B 49 26 CE 65 BD 2F 86 8D 9D 0C 34 1B 5E 32 6E EF 60 4B E1 60 7F 1A 98 CF 14 42 85 A6 F8 BE A5 EE A7 A6 C7 9E 11 20 FB AE FA 95 0A 20 B7 87 A4 8F 0E 20 FB AE FA 9D 0A 20 E5 B6 95 B0 0A 28 50 28 90 3F 28 BB 03 28 50 40 00 4A 10 4E 64 43 67 6D 61 71 35 6D 52 73 43 53 38 41 58 52 68 AF 63 72 0B 4D 5B 17 6E D8 35 C1 D3 3F C8 D7 FC F0 A8 0A 67 4D B5 A6 B3 B7 E2 E1 9F 96 68 D3 BC AD 4A 6A 20 72 E8 D2 44 C3 8B 93 60 F3 3C 4B 46 83 E4 75 A2 3C 72 A4 F7 31 D9 88 89 23 34 9A AF EF FC 17 29 5D 6C D0 2B F1 63 D5 9F E2 B9 B5 49 D2 62 E3 D0 F9 19 C5 0D 20 AF 78 D5 34 7E BB B7 E2 8E 5C 69 F4 38 38 E7 +Packet 20:02:50 : sequenceId = 57485 +Packet 20:02:50 : sso(inner)extraData = +Packet Debug 20:02:50 : commandName=MultiMsg.ApplyUp +找不到包 PacketFactory +Packet 20:02:50 : 传递给 PacketFactory 的数据 = 00 00 01 88 08 01 12 FF 02 08 00 12 40 4F 54 39 4E 4C 63 47 31 5A 79 79 62 78 69 39 30 76 77 2F 63 30 59 7A 30 42 4A 69 45 63 4B 4A 32 32 36 43 42 55 6B 56 46 49 74 73 7A 34 51 5A 73 55 78 7A 75 71 5A 53 2F 78 33 66 50 76 53 4C 74 1A 98 01 1B 76 62 FB B2 C6 24 C3 1F 39 47 0D 45 5C 77 BD 0C 8F 69 FB C8 4F D8 76 83 26 60 EA A3 24 BC FD F6 C8 B4 64 DA 47 9D 6C 1A FA F4 EF 02 FC A4 76 1F 87 EB FF 51 62 20 E9 1F 74 6B 2F 7B 7C 53 EC 6D A2 53 AC 2B 93 B4 79 83 6D E6 D8 86 E1 D5 E2 4D EE 75 03 A3 3B 72 EB 0A 3E 13 3A 80 70 EF CC B4 0D F9 42 E3 DF 5F 7A 4C 36 BC 3B 9C 31 5A B1 40 B4 5B 49 26 CE 65 BD 2F 86 8D 9D 0C 34 1B 5E 32 6E EF 60 4B E1 60 7F 1A 98 CF 14 42 85 A6 F8 BE A5 EE A7 A6 C7 9E 11 20 FB AE FA 95 0A 20 B7 87 A4 8F 0E 20 FB AE FA 9D 0A 20 E5 B6 95 B0 0A 28 50 28 90 3F 28 BB 03 28 50 40 00 4A 10 4E 64 43 67 6D 61 71 35 6D 52 73 43 53 38 41 58 52 68 AF 63 72 0B 4D 5B 17 6E D8 35 C1 D3 3F C8 D7 FC F0 A8 0A 67 4D B5 A6 B3 B7 E2 E1 9F 96 68 D3 BC AD 4A 6A 20 72 E8 D2 44 C3 8B 93 60 F3 3C 4B 46 83 E4 75 A2 3C 72 A4 F7 31 D9 88 89 23 34 9A AF EF FC 17 29 5D 6C D0 2B F1 63 D5 9F E2 B9 B5 49 D2 62 E3 D0 F9 19 C5 0D 20 AF 78 D5 34 7E BB B7 E2 8E 5C 69 F4 38 38 E7 +Packet 20:02:50 : 不是oicq response(可能是 UNI/PB)= +Packet 20:02:50 : =======================共有 0 个包======================= + */ + +internal class MultiMsg { + + object ApplyUp : OutgoingPacketFactory("MultiMsg.ApplyUp") { + 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)" + } + } + + object MessageTooLarge : Response() + } + + // captured from group + fun createForGroupLongMessage( + client: QQAndroidClient, + messageData: MessageValidationData, + dstUin: Long // group uin + ): OutgoingPacket = buildOutgoingUniPacket(client) { + writeProtoBuf( + MultiMsg.ReqBody.serializer(), + MultiMsg.ReqBody( + buType = 1, + buildVer = "8.2.0.1296", + multimsgApplyupReq = listOf( + MultiMsg.MultiMsgApplyUpReq( + applyId = 0, + dstUin = dstUin, + msgMd5 = messageData.md5, + 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, + ) + ) + } + + /* + RspBody#195600860 { + multimsgApplyupRsp=[MultiMsgApplyUpRsp#314337396 { + applyId=0x00000000(0) + blockSize=0x0000000000000000(0) + msgKey=4E 64 43 67 6D 61 71 35 6D 52 73 43 53 38 41 58 + msgResid=4F 54 39 4E 4C 63 47 31 5A 79 79 62 78 69 39 30 76 77 2F 63 30 59 7A 30 42 4A 69 45 63 4B 4A 32 32 36 43 42 55 6B 56 46 49 74 73 7A 34 51 5A 73 55 78 7A 75 71 5A 53 2F 78 33 66 50 76 53 4C 74 + msgSig=AF 63 72 0B 4D 5B 17 6E D8 35 C1 D3 3F C8 D7 FC F0 A8 0A 67 4D B5 A6 B3 B7 E2 E1 9F 96 68 D3 BC AD 4A 6A 20 72 E8 D2 44 C3 8B 93 60 F3 3C 4B 46 83 E4 75 A2 3C 72 A4 F7 31 D9 88 89 23 34 9A AF EF FC 17 29 5D 6C D0 2B F1 63 D5 9F E2 B9 B5 49 D2 62 E3 D0 F9 19 C5 0D 20 AF 78 D5 34 7E BB B7 E2 8E 5C 69 F4 38 38 E7 + msgUkey=1B 76 62 FB B2 C6 24 C3 1F 39 47 0D 45 5C 77 BD 0C 8F 69 FB C8 4F D8 76 83 26 60 EA A3 24 BC FD F6 C8 B4 64 DA 47 9D 6C 1A FA F4 EF 02 FC A4 76 1F 87 EB FF 51 62 20 E9 1F 74 6B 2F 7B 7C 53 EC 6D A2 53 AC 2B 93 B4 79 83 6D E6 D8 86 E1 D5 E2 4D EE 75 03 A3 3B 72 EB 0A 3E 13 3A 80 70 EF CC B4 0D F9 42 E3 DF 5F 7A 4C 36 BC 3B 9C 31 5A B1 40 B4 5B 49 26 CE 65 BD 2F 86 8D 9D 0C 34 1B 5E 32 6E EF 60 4B E1 60 7F 1A 98 CF 14 42 85 A6 F8 BE A5 EE A7 A6 C7 9E 11 + result=0x00000000(0) + uint32UpIp=[0xA2BE977B(-1564567685), 0xE1E903B7(-504822857), 0xA3BE977B(-1547790469), 0xA6055B65(-1509598363)] + uint32UpPort=[0x00000050(80), 0x00001F90(8080), 0x000001BB(443), 0x00000050(80)] + upOffset=0x0000000000000000(0) + }] + subcmd=0x00000001(1) + } + */ + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { + 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}" }) + } + } + } + } +} \ 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-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt index d4c61ba14..b11c38266 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt @@ -38,7 +38,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.buildResponseUniPacket import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.debug import net.mamoe.mirai.utils.io.read -import net.mamoe.mirai.utils.io.readString +import net.mamoe.mirai.qqandroid.utils.io.readString import net.mamoe.mirai.utils.io.toUHexString internal class OnlinePush { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt index 8df0f0188..c8bc93354 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt @@ -20,8 +20,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.* import net.mamoe.mirai.qqandroid.utils.GuidSource import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag import net.mamoe.mirai.qqandroid.utils.guidFlag +import net.mamoe.mirai.qqandroid.utils.io.* import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.cryptor.TEA +import net.mamoe.mirai.qqandroid.utils.cryptor.TEA import net.mamoe.mirai.utils.io.* internal class WtLogin { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt similarity index 98% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt index 0b93f83b5..85434c3ab 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.utils.cryptor +package net.mamoe.mirai.qqandroid.utils.cryptor import net.mamoe.mirai.utils.io.chunkedHexToBytes diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/TEA.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt similarity index 99% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/TEA.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt index 3e43b3fde..ea889b47c 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/TEA.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.utils.cryptor +package net.mamoe.mirai.qqandroid.utils.cryptor import kotlinx.io.core.ByteReadPacket import kotlinx.io.pool.useInstance diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/input.kt similarity index 96% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/input.kt index 128fffbd0..4471a7be8 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/input.kt @@ -11,7 +11,7 @@ @file:JvmMultifileClass @file:JvmName("Utils") -package net.mamoe.mirai.utils.io +package net.mamoe.mirai.qqandroid.utils.io import kotlinx.io.OutputStream import kotlinx.io.charsets.Charset @@ -27,6 +27,9 @@ import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic import kotlinx.serialization.InternalSerializationApi +import net.mamoe.mirai.utils.io.ByteArrayPool +import net.mamoe.mirai.utils.io.toReadPacket +import net.mamoe.mirai.utils.io.toUHexString @OptIn(MiraiInternalAPI::class, InternalSerializationApi::class) fun ByteReadPacket.copyTo(outputStream: OutputStream) { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt similarity index 92% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt index 803e579a6..e6579d96d 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt @@ -11,12 +11,12 @@ @file:JvmMultifileClass @file:JvmName("Utils") -package net.mamoe.mirai.utils.io +package net.mamoe.mirai.qqandroid.utils.io import kotlinx.io.core.* +import net.mamoe.mirai.qqandroid.utils.coerceAtMostOrFail +import net.mamoe.mirai.qqandroid.utils.cryptor.TEA import net.mamoe.mirai.utils.MiraiInternalAPI -import net.mamoe.mirai.utils.coerceAtMostOrFail -import net.mamoe.mirai.utils.cryptor.TEA import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -41,7 +41,7 @@ inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int { inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int = BytePacketBuilder().apply(builder).build().use { if (tag != null) writeUByte(tag) - val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL) + val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL) writeInt(length.toInt()) writePacket(it) return length.toInt() @@ -50,7 +50,7 @@ inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int = BytePacketBuilder().apply(builder).build().use { if (tag != null) writeUByte(tag) - val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFL) + val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL) writeUShort(length.toUShort()) writePacket(it) return length.toInt() diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/numbers.kt similarity index 70% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/numbers.kt index 079f857b1..856584958 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/numbers.kt @@ -10,7 +10,7 @@ @file:JvmMultifileClass @file:JvmName("Utils") -package net.mamoe.mirai.utils +package net.mamoe.mirai.qqandroid.utils import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -18,8 +18,8 @@ import kotlin.jvm.JvmName /** * 要求 [this] 最小为 [min]. */ -@Suppress("NOTHING_TO_INLINE") -inline fun Int.coerceAtLeastOrFail(min: Int): Int { +@PublishedApi +internal fun Int.coerceAtLeastOrFail(min: Int): Int { require(this >= min) return this } @@ -27,8 +27,8 @@ inline fun Int.coerceAtLeastOrFail(min: Int): Int { /** * 要求 [this] 最小为 [min]. */ -@Suppress("NOTHING_TO_INLINE") -inline fun Long.coerceAtLeastOrFail(min: Long): Long { +@PublishedApi +internal fun Long.coerceAtLeastOrFail(min: Long): Long { require(this >= min) return this } @@ -36,12 +36,12 @@ inline fun Long.coerceAtLeastOrFail(min: Long): Long { /** * 要求 [this] 最大为 [max]. */ -@Suppress("NOTHING_TO_INLINE") -inline fun Int.coerceAtMostOrFail(max: Int): Int = +@PublishedApi +internal fun Int.coerceAtMostOrFail(max: Int): Int = if (this >= max) error("value is greater than its expected maximum value $max") else this -@Suppress("NOTHING_TO_INLINE") -inline fun Long.coerceAtMostOrFail(max: Long): Long = +@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/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt b/mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt similarity index 93% rename from mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt rename to mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt index 0cb60c7e9..04f36145f 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt +++ b/mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -package net.mamoe.mirai.utils.cryptor +package net.mamoe.mirai.qqandroid.utils.cryptor import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiPlatformUtils @@ -27,11 +27,13 @@ internal actual class ECDHKeyPairImpl( override val privateKey: ECDHPrivateKey get() = delegate.private override val publicKey: ECDHPublicKey get() = delegate.public - override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) + override val initialShareKey: ByteArray = + ECDH.calculateShareKey(privateKey, initialPublicKey) } @Suppress("FunctionName") -actual fun ECDH() = ECDH(ECDH.generateKeyPair()) +actual fun ECDH() = + ECDH(ECDH.generateKeyPair()) actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt index 4e4de1eff..ea2c41cd1 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt @@ -21,6 +21,8 @@ import java.io.InputStream import java.net.Inet4Address import java.security.MessageDigest import java.util.zip.Deflater +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream import java.util.zip.Inflater @@ -63,6 +65,7 @@ actual object MiraiPlatformUtils { } } + actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray { data.checkOffsetAndLength(offset, length) return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest() @@ -99,4 +102,18 @@ actual object MiraiPlatformUtils { block(read) } } + + actual fun gzip(data: ByteArray, offset: Int, length: Int): ByteArray { + ByteArrayOutputStream().use { buf -> + GZIPOutputStream(buf).use { gzip -> + data.inputStream(offset, length).use { t -> t.copyTo(gzip) } + } + buf.flush() + return buf.toByteArray() + } + } + + actual fun ungzip(data: ByteArray, offset: Int, length: Int): ByteArray { + return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt index b509acdf6..bc99d768f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Job import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.data.* +import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI @@ -139,6 +140,14 @@ interface LowLevelBotAPIAccessor { @LowLevelAPI @MiraiExperimentalAPI suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData + + /** + * 发送长消息 + */ + @SinceMirai("0.31.0") + @LowLevelAPI + @MiraiExperimentalAPI + suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) } /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt index 6aad7bbd9..c31a8d3cd 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt @@ -16,6 +16,7 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.message.data.NullMessageChain.equals import net.mamoe.mirai.message.data.NullMessageChain.toString import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.SinceMirai import kotlin.js.JsName import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -45,6 +46,12 @@ interface MessageChain : Message, Iterable { override operator fun contains(sub: String): Boolean override fun toString(): String + /** + * 元素数量 + */ + @SinceMirai("0.31.1") + val size: Int + /** * 获取第一个类型为 [key] 的 [Message] 实例 * @@ -374,7 +381,6 @@ inline fun MessageChain.flatten(): Sequence = this.asSequence() / // endregion converters -// region implementations /** * 不含任何元素的 [MessageChain] @@ -389,19 +395,25 @@ object EmptyMessageChain : MessageChain by MessageChainImplByIterable(emptyList( */ object NullMessageChain : MessageChain { override fun toString(): String = "NullMessageChain" + override val size: Int get() = 0 override fun equals(other: Any?): Boolean = other === this override fun contains(sub: String): Boolean = error("accessing NullMessageChain") override fun followedBy(tail: Message): CombinedMessage = CombinedMessage(left = EmptyMessageChain, tail = tail) override fun iterator(): MutableIterator = error("accessing NullMessageChain") } + +// region implementations + + /** * 使用 [Iterable] 作为委托的 [MessageChain] */ @PublishedApi -internal inline class MessageChainImplByIterable constructor( +internal class MessageChainImplByIterable constructor( private val delegate: Iterable ) : Message, Iterable, MessageChain { + override val size: Int by lazy { delegate.count() } override fun iterator(): Iterator = delegate.iterator() override fun toString(): String = this.delegate.joinToString("") { it.toString() } override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) } @@ -411,9 +423,10 @@ internal inline class MessageChainImplByIterable constructor( * 使用 [Collection] 作为委托的 [MessageChain] */ @PublishedApi -internal inline class MessageChainImplByCollection constructor( +internal class MessageChainImplByCollection constructor( private val delegate: Collection ) : Message, Iterable, MessageChain { + override val size: Int get() = delegate.size override fun iterator(): Iterator = delegate.iterator() override fun toString(): String = this.delegate.joinToString("") { it.toString() } override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) } @@ -426,11 +439,12 @@ internal inline class MessageChainImplByCollection constructor( internal class MessageChainImplBySequence constructor( delegate: Sequence ) : Message, Iterable, MessageChain { + override val size: Int by lazy { collected.size } + /** * [Sequence] 可能只能消耗一遍, 因此需要先转为 [List] */ private val collected: List by lazy { delegate.toList() } - override fun iterator(): Iterator = collected.iterator() override fun toString(): String = this.collected.joinToString("") { it.toString() } override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) } @@ -440,9 +454,10 @@ internal class MessageChainImplBySequence constructor( * 单个 [SingleMessage] 作为 [MessageChain] */ @PublishedApi -internal inline class SingleMessageChainImpl constructor( +internal class SingleMessageChainImpl constructor( private val delegate: SingleMessage ) : Message, Iterable, MessageChain { + override val size: Int get() = 1 override fun toString(): String = this.delegate.toString() override fun iterator(): Iterator = iterator { yield(delegate) } override operator fun contains(sub: String): Boolean = sub in delegate diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt index 950d05920..ec890d3b9 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt @@ -15,7 +15,6 @@ package net.mamoe.mirai.message.data import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName -import kotlin.jvm.JvmStatic import kotlin.jvm.JvmSynthetic /** @@ -28,26 +27,13 @@ class PlainText(val stringValue: String) : Comparable by stringValue, CharSequence by stringValue { + @Suppress("unused") constructor(charSequence: CharSequence) : this(charSequence.toString()) override operator fun contains(sub: String): Boolean = sub in stringValue override fun toString(): String = stringValue - companion object Key : Message.Key { - @JvmStatic - val Empty = PlainText("") - - @JvmStatic - val Null = PlainText("null") - - inline fun of(value: String): PlainText { - return PlainText(value) - } - - inline fun of(value: CharSequence): PlainText { - return PlainText(value) - } - } + companion object Key : Message.Key<PlainText> } /** 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 26e49d506..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 @@ -33,6 +34,48 @@ interface RichMessage : MessageContent { @SinceMirai("0.30.0") companion object Templates : Message.Key<RichMessage> { + /** + * 合并转发. + */ + @MiraiExperimentalAPI + fun mergedForward(): Nothing { + TODO() + } + + /** + * 长消息. + * + * @param brief 消息内容纯文本, 显示在图片的前面 + */ + @SinceMirai("0.31.0") + @OptIn(MiraiInternalAPI::class) + @MiraiExperimentalAPI + fun longMessage(brief: String, resId: String, timeSeconds: Long): RichMessage { + val limited: String = if (brief.length > 30) { + brief.take(30) + "…" + } else { + brief + } + + val template = """ + <?xml version='1.0' encoding='UTF-8' standalone='yes' ?> + <msg serviceID="35" templateID="1" action="viewMultiMsg" + brief="$limited" + m_resid="$resId" + m_fileName="$timeSeconds" sourceMsgId="0" url="" + flag="3" adverSign="0" multiMsgFlag="1"> + <item layout="1"> + <title>$limited</title> + <hr hidden="false" style="0"/> + <summary>点击查看完整消息</summary> + </item> + <source name="聊天记录" icon="" action="" appid="-1"/> + </msg> + """.trimIndent() + + return LongMessage(template, resId) + } + @MiraiExperimentalAPI @SinceMirai("0.30.0") fun share(url: String, title: String? = null, content: String? = null, coverUrl: String? = null): XmlMessage = @@ -107,6 +150,20 @@ class XmlMessage constructor(override val content: String) : RichMessage { override fun toString(): String = content } +/** + * 长消息 + */ +@SinceMirai("0.31.0") +@MiraiExperimentalAPI +@MiraiInternalAPI +class LongMessage(override val content: String, val resId: String) : RichMessage { + companion object Key : Message.Key<XmlMessage> + + // serviceId = 35 + override fun toString(): String = content +} + + /** * 构造一条 XML 消息 */ 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> = 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/platform.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt index fd5f8a083..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 @@ -30,6 +30,10 @@ expect object MiraiPlatformUtils { fun zip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray + fun gzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray + + fun ungzip(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray + fun md5(data: ByteArray, offset: Int = 0, length: Int = data.size - offset): ByteArray diff --git a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt index 9bf4d397b..cead3f84e 100644 --- a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt +++ b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/utils/PlatformUtilsTest.kt @@ -14,11 +14,16 @@ import net.mamoe.mirai.utils.io.encodeToString import kotlin.test.Test import kotlin.test.assertEquals +@OptIn(MiraiInternalAPI::class) internal class PlatformUtilsTest { - @OptIn(MiraiInternalAPI::class) @Test fun testZip() { assertEquals("test", MiraiPlatformUtils.unzip(MiraiPlatformUtils.zip("test".toByteArray())).encodeToString()) } + + @Test + fun testGZip() { + assertEquals("test", MiraiPlatformUtils.ungzip(MiraiPlatformUtils.gzip("test".toByteArray())).encodeToString()) + } } \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt index e142b46d1..d5af19847 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt @@ -22,6 +22,8 @@ import java.io.OutputStream import java.net.Inet4Address import java.security.MessageDigest import java.util.zip.Deflater +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream import java.util.zip.Inflater /** @@ -64,6 +66,20 @@ actual object MiraiPlatformUtils { } } + actual fun gzip(data: ByteArray, offset: Int, length: Int): ByteArray { + ByteArrayOutputStream().use { buf -> + GZIPOutputStream(buf).use { gzip -> + data.inputStream(offset, length).use { t -> t.copyTo(gzip) } + } + buf.flush() + return buf.toByteArray() + } + } + + actual fun ungzip(data: ByteArray, offset: Int, length: Int): ByteArray { + return GZIPInputStream(data.inputStream(offset, length)).use { it.readBytes() } + } + actual fun md5(data: ByteArray, offset: Int, length: Int): ByteArray { data.checkOffsetAndLength(offset, length) return MessageDigest.getInstance("MD5").apply { update(data, offset, length) }.digest()